@bongos/core 1.21.113 → 1.21.115

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 +608 -188
  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 +709 -626
  25. package/docs/copy-registry.json +1673 -858
  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 +4875 -4748
  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-hall-live.states.json +0 -5
  166. package/modules/public-landing/public/projects.html +169 -6
  167. package/modules/public-landing/public/projects.probes.json +10 -6
  168. package/modules/public-landing/public/projects.states.json +13 -13
  169. package/modules/public-landing/public/terms.html +2 -2
  170. package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
  171. package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
  172. package/modules/ui-design/kit/fixtures/connections.json +22 -0
  173. package/modules/ui-design/kit/serve.js +95 -0
  174. package/package-lock.json +2 -2
  175. package/package.json +1 -1
  176. package/release-notes.json +128 -0
  177. package/scripts/discord/README.md +8 -5
  178. package/scripts/discord/post.js +12 -1
  179. package/scripts/gds/artifact-audit.js +20 -3
  180. package/scripts/gds/ask.js +164 -0
  181. package/scripts/gds/autobongos-verify.js +21 -2
  182. package/scripts/gds/government-walk.js +638 -4
  183. package/scripts/gds/pulse.js +9 -6
  184. package/scripts/gds/ship-escalation.js +22 -3
  185. package/scripts/gds/ship-finish.js +5 -4
  186. package/scripts/gds/ship-merge-gate.js +18 -6
  187. package/scripts/hall-preview/README.md +41 -1
  188. package/scripts/hall-preview/pref-writes.js +140 -0
  189. package/scripts/hall-preview/refresh-fixtures.js +101 -7
  190. package/scripts/hall-preview/scrub-identity.js +366 -7
  191. package/scripts/hall-preview/server.js +27 -1
  192. package/scripts/render-diagrams.sh +4 -2
  193. package/src/bongos/auth-idp.js +6 -1
  194. package/src/bongos/project-settings.js +41 -0
  195. package/src/bongos/route-rank-check.js +27 -0
  196. package/src/bongos/routes/me.js +68 -7
  197. package/src/bongos/serve-internal.js +5 -1
  198. package/src/module-api.js +7 -1
  199. package/src/platform-server.js +10 -0
  200. package/tests/application_lifecycle.mjs +47 -5
  201. package/tests/conductor_main_e2e.mjs +21 -0
  202. package/tests/connections_api.mjs +18 -0
  203. package/tests/craft_mode_nav.mjs +5 -3
  204. package/tests/craft_mode_tallies.mjs +52 -16
  205. package/tests/discord_approvals.mjs +11 -0
  206. package/tests/discord_channels.mjs +20 -0
  207. package/tests/fixtures/gov-seated.mjs +64 -0
  208. package/tests/fixtures/government-ask-cards.json +1284 -0
  209. package/tests/government_act_alone_buildings.mjs +1 -0
  210. package/tests/government_agent_asks.mjs +322 -0
  211. package/tests/government_asks.mjs +408 -0
  212. package/tests/government_build_rights.mjs +253 -0
  213. package/tests/government_buildouts.mjs +382 -0
  214. package/tests/government_counts.mjs +143 -0
  215. package/tests/government_decision_rules.mjs +5 -1
  216. package/tests/government_laws.mjs +488 -0
  217. package/tests/government_laws_session.mjs +321 -0
  218. package/tests/government_leads.mjs +375 -0
  219. package/tests/government_positions.mjs +127 -0
  220. package/tests/government_presets.mjs +215 -0
  221. package/tests/government_routes.mjs +8 -1
  222. package/tests/government_routing.mjs +118 -0
  223. package/tests/government_seed.mjs +5 -0
  224. package/tests/government_treasury.mjs +394 -0
  225. package/tests/governor_docket.mjs +26 -2
  226. package/tests/hall_buildouts.mjs +208 -0
  227. package/tests/hall_city.mjs +14 -6
  228. package/tests/hall_city_living.mjs +617 -0
  229. package/tests/hall_gov_counts.mjs +336 -0
  230. package/tests/hall_gov_living.mjs +636 -0
  231. package/tests/hall_gov_real_site.mjs +655 -0
  232. package/tests/hall_matter_card.mjs +6 -1
  233. package/tests/hall_my_agents.mjs +481 -0
  234. package/tests/hall_nav.mjs +107 -4
  235. package/tests/hall_page_gate_map.mjs +4 -0
  236. package/tests/hall_preview_governor_fixtures.mjs +30 -3
  237. package/tests/hall_preview_scrub.mjs +59 -1
  238. package/tests/hall_preview_security.mjs +169 -0
  239. package/tests/hall_reading_world.mjs +44 -0
  240. package/tests/hall_roster_world.mjs +6 -2
  241. package/tests/hall_street.mjs +18 -2
  242. package/tests/hall_street_amend.mjs +1 -1
  243. package/tests/hall_street_deck.mjs +609 -0
  244. package/tests/hall_street_depth.mjs +559 -0
  245. package/tests/hall_street_desk.mjs +1 -1
  246. package/tests/hall_street_inside.mjs +29 -5
  247. package/tests/hall_tweak_editor.mjs +4 -1
  248. package/tests/hub_account_page.mjs +94 -3
  249. package/tests/hub_connections_page.mjs +507 -0
  250. package/tests/hub_invite_badge.mjs +1 -1
  251. package/tests/idp_device_exchange.mjs +18 -0
  252. package/tests/landing_legal_pages.mjs +8 -5
  253. package/tests/landing_page.mjs +23 -4
  254. package/tests/nav_permission_atoms.mjs +4 -1
  255. package/tests/profile_connections_ui.mjs +88 -25
  256. package/tests/projects_hub.mjs +15 -0
  257. package/tests/projects_hub_pre_uat.mjs +4 -2
  258. package/tests/render_check.mjs +2 -2
  259. package/tests/ship_broadcast_routing.mjs +57 -16
  260. package/tests/ship_broadcast_server.mjs +40 -7
  261. package/tests/ship_merge_gate.mjs +19 -0
  262. package/tests/ui_design_kit.mjs +2 -2
  263. package/tests/watch_applications_queue.mjs +23 -0
  264. package/tests/wizard_physics.mjs +1 -1
  265. package/tests/wizard_physics_more.mjs +133 -0
@@ -31,6 +31,54 @@ const { validateOrRespond, LIMITS, parseId } = api;
31
31
  // reading, contributed from this factory (see ../governor-docket.js).
32
32
  const governorDocket = require('../governor-docket');
33
33
 
34
+ // THE ASK (task 1004703). A blocker is a request from an agent to a person, so
35
+ // it may carry an ASK in the one shape every such request takes: a question of
36
+ // at most twelve words, two or three tap answers with a one-line result each, a
37
+ // pick and why, the seat it waits on, what happens if nobody answers. The shape
38
+ // is the government module's (modules/government/asks.js), reached through its
39
+ // `government` port: checked BEFORE the blocker is filed (a slop ask files
40
+ // nothing and answers 400 `ask_refused`, naming every fix in plain words), and
41
+ // stored against the blocker's docket id `blockers:<id>`. The blocker's own
42
+ // title and body stay as they are: the long text rides along, never as the ask.
43
+ const log = api.logger('ideas');
44
+ const governmentPort = () => (api.resolveOptional ? api.resolveOptional('government') : null);
45
+ const askMatterId = (blockerId) => `blockers:${blockerId}`;
46
+ // Each blocker with its ask, or `ask: null`. A read that fails leaves every ask
47
+ // null: the list itself never fails for want of a card.
48
+ async function withAsks(rows) {
49
+ const list = Array.isArray(rows) ? rows : [];
50
+ const gov = governmentPort();
51
+ let found = null;
52
+ if (gov && typeof gov.asksFor === 'function' && list.length) {
53
+ try { found = await gov.asksFor(list.map((b) => askMatterId(b.id))); } catch (err) {
54
+ log.warn({ err: err && err.message }, 'blockers: asks unreadable');
55
+ }
56
+ }
57
+ return list.map((b) => ({ ...b, ask: (found && found.get(askMatterId(b.id))) || null }));
58
+ }
59
+
60
+ // AGENTS WRITE ASKS (task 1004704). A blocker an AI session files BY HAND must
61
+ // carry an ask: the owner ruled that agents ask like humans, short and
62
+ // decidable, and a blocker without a card is the wall of text that ruling is
63
+ // about. "An AI session" is a CLI session (the token an agent's tools carry; the
64
+ // hall's form is a person, on a web session). "By hand" is every filing except
65
+ // the scripts that file a TEMPLATED blocker unattended, named here by the
66
+ // `source` each one sends: they keep filing whether or not they send a card, so
67
+ // an unattended run, or a checkout older than this rule, is never refused. They
68
+ // send a card too (scripts/gds/ask.js postWithAsk). An allow-list, not a
69
+ // deny-list: agents file with `task`, `manual`, or no source at all, and a
70
+ // source nobody thought of is held to the rule rather than let through.
71
+ // tests/government_agent_asks.mjs reads each script's source off the script.
72
+ // With no government module there is nothing to check a card against, so
73
+ // nothing is required.
74
+ const AGENT_SESSION_SOURCE = 'cli';
75
+ const TEMPLATED_SOURCES = Object.freeze(['no-artifact-close', 'artifact-audit', 'autobongos']);
76
+ function askRequired(req, body) {
77
+ const session = req.gdsSession;
78
+ if (!session || session.source !== AGENT_SESSION_SOURCE) return false;
79
+ return !TEMPLATED_SOURCES.includes(body.source);
80
+ }
81
+
34
82
  module.exports = function buildBlockersRouter() {
35
83
  governorDocket.contributeIdeasToDocket();
36
84
  const router = express.Router();
@@ -51,7 +99,7 @@ module.exports = function buildBlockersRouter() {
51
99
  blockers.countOpenBlockers(),
52
100
  ]);
53
101
  res.set('Cache-Control', 'public, max-age=60');
54
- res.json({ blockers: rows, page: api.pageMeta(limit, offset, total) });
102
+ res.json({ blockers: await withAsks(rows), page: api.pageMeta(limit, offset, total) });
55
103
  } catch (err) {
56
104
  console.error('[gds] GET /blockers', err);
57
105
  res.fail('list_failed', { status: 500, message: 'internal error' });
@@ -64,18 +112,46 @@ module.exports = function buildBlockersRouter() {
64
112
  if (id === null) return;
65
113
  const blocker = await blockers.getBlocker(id);
66
114
  if (!blocker) return res.fail('blocker_not_found', 404);
67
- res.json({ blocker });
115
+ res.json({ blocker: (await withAsks([blocker]))[0] });
68
116
  });
69
117
 
70
118
  // rank: any-builder — FILE a blocker (contribution, not resolution).
119
+ // Body: { title, body_md?, source?, source_ref?, ask? } — `ask` is the card a
120
+ // person answers (task 1004703, see THE ASK above); a slop ask files nothing
121
+ // (400 `ask_refused`). An AI session filing by hand (a CLI session, any `source`
122
+ // but a templated script's) must send one: without it, 400 `ask_required` (task 1004704).
123
+ // → 201 { blocker, ask } (ask: the stored card, or null).
71
124
  router.post('/blockers', api.requireBuilder, async (req, res) => {
72
125
  if (validateOrRespond(req, res, {
73
126
  title: { required: true, type: 'string', maxLength: LIMITS.TITLE, minLength: 1 },
74
127
  body_md: { type: 'string', maxLength: LIMITS.DESCRIPTION },
75
128
  source: { type: 'string', maxLength: LIMITS.SOURCE },
76
129
  source_ref: { type: 'string', maxLength: LIMITS.SOURCE_REF },
130
+ ask: { type: 'object' },
77
131
  })) return;
78
132
  const body = req.body || {};
133
+ let verdict = null;
134
+ let gov = null;
135
+ if ((body.ask === undefined || body.ask === null) && askRequired(req, body)) {
136
+ const port = governmentPort();
137
+ if (port && typeof port.checkAsk === 'function') {
138
+ return res.fail('ask_required', {
139
+ status: 400,
140
+ message: port.askRequiredMessage || 'An AI session files a blocker with an ask. Check the card first with: node scripts/gds/ask.js check <file>',
141
+ details: { field: 'ask', helper: 'node scripts/gds/ask.js check <file>' },
142
+ });
143
+ }
144
+ }
145
+ if (body.ask !== undefined && body.ask !== null) {
146
+ gov = governmentPort();
147
+ if (!gov || typeof gov.checkAsk !== 'function') {
148
+ return res.fail('asks_unavailable', { status: 503, message: 'Asks cannot be checked here just now, so nothing was filed. Try again, or file it without the ask.' });
149
+ }
150
+ verdict = gov.checkAsk(body.ask);
151
+ if (!verdict.ok) {
152
+ return res.fail('ask_refused', { status: 400, message: gov.refusalLine(verdict.errors), details: { errors: verdict.errors } });
153
+ }
154
+ }
79
155
  try {
80
156
  const blocker = await blockers.createBlocker({
81
157
  title: body.title,
@@ -87,7 +163,17 @@ module.exports = function buildBlockersRouter() {
87
163
  // ON CONFLICT — same (source, source_ref) already captured.
88
164
  return res.status(200).json({ skipped: true, reason: 'duplicate_source_ref' });
89
165
  }
90
- res.status(201).json({ blocker });
166
+ let ask = null;
167
+ if (verdict) {
168
+ try {
169
+ ask = await gov.saveAsk({ matterId: askMatterId(blocker.id), ask: verdict.ask, builderId: req.builder.id });
170
+ } catch (err) {
171
+ // the blocker stands; say plainly that its card did not
172
+ log.error({ err: err && err.message }, 'POST /blockers: ask not saved');
173
+ return res.status(201).json({ blocker, ask: null, ask_saved: false });
174
+ }
175
+ }
176
+ res.status(201).json({ blocker, ask });
91
177
  } catch (err) {
92
178
  if (err.code === 'TITLE_REQUIRED') return res.fail('title_required', 400);
93
179
  console.error('[gds] POST /blockers', err);
@@ -193,3 +279,6 @@ module.exports = function buildBlockersRouter() {
193
279
 
194
280
  return router;
195
281
  };
282
+
283
+ // The scripts whose templated blockers file with or without a card (task 1004704).
284
+ module.exports.TEMPLATED_SOURCES = TEMPLATED_SOURCES;
@@ -43,7 +43,8 @@
43
43
  "lifecycle",
44
44
  "lifecycle.kickoffSeed",
45
45
  "lifecycle.removals",
46
- "lifecycle.workflowDispatch"
46
+ "lifecycle.workflowDispatch",
47
+ "lifecycle.traffic"
47
48
  ],
48
49
  "consumes": []
49
50
  }
@@ -39,6 +39,22 @@ async function countMoving(deps = {}) {
39
39
  return { moving: active + confirmed, active, confirmed };
40
40
  }
41
41
 
42
+ // → how many agents are at work: one per open claim, the rows GET /tasks/claimable
43
+ // lists as `active_claims` (db-tasks.js listActiveClaims, same joins), counted in SQL
44
+ // for GET /government/counts (task 1004716), which reads it with countMoving above
45
+ // through the `lifecycle.traffic` port (routes/moving.js registers it).
46
+ async function countAgentsAtWork(deps = {}) {
47
+ const pool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : api.pool;
48
+ const { rows } = await pool.query(
49
+ `SELECT count(*)::int AS n
50
+ FROM claims c
51
+ JOIN tasks t ON t.id = c.task_id
52
+ JOIN builders b ON b.id = c.builder_id
53
+ WHERE c.released_at IS NULL`,
54
+ );
55
+ return Number(rows[0] && rows[0].n) || 0;
56
+ }
57
+
42
58
  // The GET /moving handler, injectable so a test drives it with no express.
43
59
  function movingHandler({ log, count = countMoving } = {}) {
44
60
  return async function movingRoute(_req, res) {
@@ -52,4 +68,4 @@ function movingHandler({ log, count = countMoving } = {}) {
52
68
  };
53
69
  }
54
70
 
55
- module.exports = { MOVING_STATUSES, countMoving, movingHandler };
71
+ module.exports = { MOVING_STATUSES, countMoving, countAgentsAtWork, movingHandler };
@@ -20,6 +20,12 @@ const log = api.logger('lifecycle');
20
20
 
21
21
  module.exports = function buildMovingRouter() {
22
22
  const router = express.Router();
23
+ // The `lifecycle.traffic` port (task 1004716): the two traffic counts, for GET
24
+ // /government/counts to read without importing this module. hasProvider-guarded
25
+ // so a test rebuilding the router never registers it twice.
26
+ if (!api.hasProvider('lifecycle.traffic')) {
27
+ api.registerProvider('lifecycle.traffic', { countMoving: moving.countMoving, countAgentsAtWork: moving.countAgentsAtWork });
28
+ }
23
29
 
24
30
  // rank: any-builder — GET /moving, the count behind Governing mode's "moving" tally.
25
31
  // A count of rows every signed-in builder can already list on the Tasks board, so no
@@ -381,6 +381,29 @@ module.exports = function buildProjectSettingsRouter() {
381
381
  }
382
382
  });
383
383
 
384
+ // PATCH /project-settings/spend-cap — body { usd: number | null } (task 1004712,
385
+ // ADR 0379). The owner's ruling: NO spend limit by default; a cap is this
386
+ // optional setting, off until set. null turns it off (the row is deleted).
387
+ //
388
+ // rank: project.settings.manage (metic+, government_010), the same atom as the
389
+ // other writes on this page; who decides at the Treasury may put it to a sitting
390
+ // (requireActAlone). GET /government/treasury reads it back as `cap`.
391
+ router.patch('/project-settings/spend-cap', api.requireBuilder, api.requirePermission('project.settings.manage'), api.requireActAlone('treasury'), async (req, res) => {
392
+ if (api.validateOrRespond(req, res, { usd: {} })) return; // a number of dollars, or null to turn it off; parsed below
393
+ const raw = req.body ? req.body.usd : undefined;
394
+ if (raw === undefined) return res.fail('bad_spend_cap', { status: 400, message: 'usd: send a number of dollars a month, or null to turn the cap off' });
395
+ const parsed = raw === null ? { usd: null } : api.projectSettings.parseSpendCap(raw);
396
+ if (parsed.reason) return res.fail('bad_spend_cap', { status: 400, message: `usd: ${parsed.reason}` });
397
+ try {
398
+ await api.projectSettings.setSpendCap(parsed.usd, req.builder.id);
399
+ const resolved = await api.projectSettings.resolveSpendCap();
400
+ res.json({ ok: true, on: resolved.on, usd: resolved.usd, sentence: api.projectSettings.describeSpendCap(resolved) });
401
+ } catch (err) {
402
+ log.error(`PATCH /project-settings/spend-cap: ${err && err.message}`);
403
+ res.fail('project_settings_write_failed', { status: 500, message: 'internal error' });
404
+ }
405
+ });
406
+
384
407
  // --- tweak auto-pickup (task 1004677, ADR 0371 D3/D5) ----------------------
385
408
  //
386
409
  // GET /project-settings/tweak-autopickup — the switch, the monthly cap, and how
@@ -25,11 +25,19 @@
25
25
 
26
26
  // PURE — the #member-approvals request line for a pending access request.
27
27
  // `request` is the access_requests row: { id, github_login, display_name, note }.
28
+ // `refiled: true` is the line for a vouched re-application that took over the
29
+ // pending row (ADR 0333 D4): the webhook cannot edit the first line, so a second
30
+ // one carries the row's words now. Both lead with the same marker, so a 👍 on
31
+ // either resolves the one request.
28
32
  function memberApprovalLine(request) {
29
33
  const who = request.display_name
30
34
  ? `${request.github_login} (${request.display_name})`
31
35
  : request.github_login;
32
36
  const note = request.note ? ` — “${String(request.note).slice(0, 200)}”` : '';
37
+ if (request.refiled) {
38
+ return `member-request #${request.id} — ${who} asked again with a vouched identity${note}\n`
39
+ + `These words replace the earlier line for #${request.id}. React 👍 to invite them, 👎 to dismiss.`;
40
+ }
33
41
  return `member-request #${request.id} — ${who} is asking to join the hall${note}\n`
34
42
  + `React 👍 to invite them, 👎 to dismiss.`;
35
43
  }
@@ -96,14 +96,16 @@ const APPLICATION_VOUCH_HEADER = 'X-Bongos-Platform-Assertion';
96
96
  // vouchPendingRow — a vouched re-application takes over an UNVOUCHED pending row
97
97
  // for its own login, name and note included, so a row someone else filed under
98
98
  // that login cannot pre-empt the real applicant's vouch (ADR 0333 D4). A row
99
- // already vouched is never re-stamped.
100
- function vouchPendingRow({ login, displayName, note, vouch }) {
101
- return pool.query(
99
+ // already vouched is never re-stamped. Returns the row it took over, else null.
100
+ async function vouchPendingRow({ login, displayName, note, vouch }) {
101
+ const { rows } = await pool.query(
102
102
  `UPDATE access_requests
103
103
  SET display_name = $2, note = $3, applicant_github_id = $4, applicant_handle = $5, vouched_at = $6
104
- WHERE lower(github_login) = lower($1) AND status = 'pending' AND applicant_github_id IS NULL`,
104
+ WHERE lower(github_login) = lower($1) AND status = 'pending' AND applicant_github_id IS NULL
105
+ RETURNING id, github_login, status, created_at`,
105
106
  [login, displayName, note, vouch.githubId, vouch.handle, new Date()],
106
107
  );
108
+ return rows[0] || null;
107
109
  }
108
110
 
109
111
  // How many applicant profiles one queue render may resolve from the hub (privacy
@@ -332,8 +334,16 @@ module.exports = function buildAccessRequestsRouter() {
332
334
  // Treat as success-ish: the Archon already has them in the queue.
333
335
  if (err.code === '23505') {
334
336
  if (vouch) {
335
- try { await vouchPendingRow({ login, displayName, note, vouch }); }
336
- catch (e) { log.error({ err: e.message }, '[gds] POST /access-requests: vouching the open request failed (non-fatal)'); }
337
+ try {
338
+ const taken = await vouchPendingRow({ login, displayName, note, vouch });
339
+ // The first #member-approvals line still shows the words the row held
340
+ // before; post the ones it holds now (same marker, same request).
341
+ if (taken) {
342
+ approvalBroadcast
343
+ .postMemberApprovalRequest({ ...taken, display_name: displayName, note, refiled: true })
344
+ .catch(() => {});
345
+ }
346
+ } catch (e) { log.error({ err: e.message }, '[gds] POST /access-requests: vouching the open request failed (non-fatal)'); }
337
347
  }
338
348
  return res.status(200).json({ ok: true, already_pending: true });
339
349
  }
@@ -293,7 +293,17 @@ async function removeConnection({ id, githubId }, { pool = defaultPool } = {}) {
293
293
  // and in theirs (D4 §2) — and in no public list, which is why this read has no
294
294
  // public twin. The returned `count` is the array's own length by construction
295
295
  // (the leak-by-subtraction rule); there is no hidden remainder.
296
- async function listConnections({ githubId, limit = FANOUT_1, offset = 0 }, { pool = defaultPool } = {}) {
296
+ //
297
+ // `here` is the PROJECT CUT a hall's profile tab reads (ADR 0337 D5.4, task
298
+ // 1004279): only the connections who are also active builders on THIS instance.
299
+ // This module runs only on the hub, so "this instance" is the hub's own project,
300
+ // and its builders are core's `builders` rows — joined on github_id (text there,
301
+ // bigint here). Both the edge and the builder row already exist; nothing records
302
+ // where a connection was made. A literal term with no new parameter, so paging
303
+ // and the FANOUT_1 cap read exactly as the full list does.
304
+ const HERE_SQL = "EXISTS (SELECT 1 FROM builders b WHERE b.github_id = e.gid::text AND b.status = 'active')";
305
+
306
+ async function listConnections({ githubId, limit = FANOUT_1, offset = 0, here = false }, { pool = defaultPool } = {}) {
297
307
  const { rows } = await pool.query(
298
308
  `WITH edge AS (
299
309
  SELECT id, addressee_github_id AS gid, resolved_at
@@ -307,7 +317,7 @@ async function listConnections({ githubId, limit = FANOUT_1, offset = 0 }, { poo
307
317
  SELECT e.id::text AS id, e.resolved_at AS connected_at, ${COUNTERPARTY_COLUMNS}
308
318
  FROM edge e
309
319
  JOIN platform_identity_accounts pa ON pa.github_id = e.gid
310
- WHERE ${accountActiveSql('pa')}
320
+ WHERE ${accountActiveSql('pa')}${here ? ` AND ${HERE_SQL}` : ''}
311
321
  ORDER BY e.resolved_at DESC NULLS LAST, e.id DESC
312
322
  LIMIT $2 OFFSET $3`,
313
323
  [githubId, Math.min(Number(limit) || FANOUT_1, FANOUT_1), Math.max(0, Number(offset) || 0)],
@@ -569,6 +579,7 @@ module.exports = {
569
579
  declineConnection,
570
580
  removeConnection,
571
581
  listConnections,
582
+ HERE_SQL,
572
583
  listRequests,
573
584
  degreeBetween,
574
585
  mutualFrontier,
@@ -16,6 +16,9 @@
16
16
  // target, because 403 confirms the handle.
17
17
  // GET /connections — my accepted connections. count is the
18
18
  // array's own length (no hidden remainder).
19
+ // `?here=1` keeps only those who also build
20
+ // on this project — the hall profile tab's
21
+ // cut (ADR 0337 D5.4, task 1004279).
19
22
  // GET /connections/requests — my inbox + outbox; stamps seen_at.
20
23
  // GET /connections/suggestions — "builders you may know" (R21, task
21
24
  // 1002297; ADR 0202 D5): mutuals + shared
@@ -107,7 +110,10 @@ module.exports = function connectionRoutes() {
107
110
  const me = await actingAccount(req.builder);
108
111
  if (me.fail) return res.fail(...me.fail);
109
112
  const { limit, offset } = parsePagination(req, { defaultLimit: conn.FANOUT_1, maxLimit: conn.FANOUT_1 });
110
- const connections = await conn.listConnections({ githubId: me.githubId, limit, offset }, { pool });
113
+ // `here=1` narrows to the connections who also build on this project (the
114
+ // hall's tab); a filter on the session's own list, never a subject.
115
+ const here = req.query.here === '1';
116
+ const connections = await conn.listConnections({ githubId: me.githubId, limit, offset, here }, { pool });
111
117
  // The count IS the array's length — never a raw total beside a filtered
112
118
  // list, which is a leak by subtraction (ADR 0171 D4 / ADR 0202 D4).
113
119
  res.json({ connections, count: connections.length });
@@ -1,5 +1,5 @@
1
1
  {
2
- "_": "The account page for a private account with its record hidden and no published profile yet (task 1004259): the recruiter copy shrinks to name and picture and says it is the one exception to 'not found', the record line says nothing has been reported and that the profile is not published, and the empty profile fields show their placeholders. stub.fixtures points the account read at fixtures/me-cross-project-private.json.",
2
+ "_": "The account page for a private account with its record hidden and no published profile yet (task 1004259): the recruiter copy shrinks to name and picture and says it is the one exception to 'not found', the record line says nothing has been reported and that the profile is not published, and the empty profile fields show their placeholders. Private and hidden both carry the guild notice (task 1004260, ADR 0337 D4.4), each listing the builder's two guilds from the stub's held set. stub.fixtures points the account read at fixtures/me-cross-project-private.json.",
3
3
  "page": "/account",
4
4
  "surface": "public-landing",
5
5
  "stub": { "fixtures": { "me__cross-project": "me-cross-project-private" } },
@@ -9,6 +9,6 @@
9
9
  "ignoreConsole": ["/api/(gds|bongos)/me/cross-project", "\\b401\\b", "Unauthorized"],
10
10
  "ignoreControls": [".vh"],
11
11
  "states": {
12
- "in-private": { "auth": true, "actions": [["wait", 400]], "expect": { "visible": ["#acctBody", "#recWhat"], "checked": "input[name=visibility][value=private]", "value": [["#fName", ""], ["#fBio", ""]], "text": [["#recordLine", "No cross-project activity reported yet. Your profile is not published yet; it publishes after your next sign-in."]] } }
12
+ "in-private": { "auth": true, "actions": [["wait", 400]], "expect": { "visible": ["#acctBody", "#recWhat", "#visGuilds", "#recordGuilds"], "count": [["#visGuildList li", 2], ["#recordGuildList li", 2]], "checked": "input[name=visibility][value=private]", "value": [["#fName", ""], ["#fBio", ""]], "text": [["#recordLine", "No cross-project activity reported yet. Your profile is not published yet; it publishes after your next sign-in."]] } }
13
13
  }
14
14
  }
@@ -63,6 +63,11 @@
63
63
  .aOpt:focus-within{outline:2px solid var(--accent);outline-offset:3px;}
64
64
  .aWhat{margin:0;font-size:13.5px;line-height:1.5;color:var(--ink-soft);max-width:70ch;}
65
65
  .aTag{font-size:12.5px;color:var(--ink-faint);}
66
+ /* the guilds that count your numbers (ADR 0337 D4.4): one line per guild, its
67
+ name the link to the page where its switch lives, its current answer beside it */
68
+ .aGuilds{display:grid;gap:6px;}
69
+ .aGuildList{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:0 22px;font-size:13.5px;line-height:1.5;}
70
+ .aGuildList a{display:inline-block;min-height:24px;padding:3px 0;color:var(--link-ink);font-weight:500;text-decoration:underline;text-underline-offset:3px;}
66
71
  .acct .actions{margin:0;gap:14px;}
67
72
  .acct .btn{min-height:44px;padding:0 22px;font-family:var(--font-body);font-weight:500;font-size:15px;text-transform:lowercase;white-space:nowrap;box-shadow:none;}
68
73
  .acct .btn:hover,.acct .btn:active{transform:none;box-shadow:none;}
@@ -115,7 +120,7 @@
115
120
  <ul class="bandRow">
116
121
  <li class="bandCell"><a class="bandT" href="/projects?view=map">Sky</a><span class="bandC">the sky of every project</span></li>
117
122
  <li class="bandCell"><a class="bandT" href="/projects?view=map&amp;sub=table">Table</a><span class="bandC">the same projects as rows</span></li>
118
- <li class="bandCell" id="mineCell"><a class="bandT" href="/projects?view=mine">Your projects</a><span class="bandC">the ones you own or build on</span></li>
123
+ <li class="bandCell" id="mineCell"><a class="bandT" href="/projects?view=mine">Your projects</a><span class="bandC">the ones you own</span></li>
119
124
  <li class="bandCell"><a class="bandT" href="/projects?view=new">New project</a><span class="bandC">five questions and a home</span></li>
120
125
  </ul>
121
126
  </div>
@@ -125,7 +130,7 @@
125
130
  <p class="bandWho" id="bandWho"><img id="bandWhoImg" alt="" hidden><span class="whoName" id="bandWhoLogin"></span></p>
126
131
  <ul class="bandRow">
127
132
  <li class="bandCell"><a class="bandT" href="/projects?view=mine">My projects</a></li>
128
- <li class="bandCell"><a class="bandT" href="/invitations">Invitations</a></li><li class="bandCell"><a class="bandT" href="/account" aria-current="page">Account</a></li>
133
+ <li class="bandCell"><a class="bandT" href="/invitations">Invitations</a></li><li class="bandCell"><a class="bandT" href="/connections">Connections</a></li><li class="bandCell"><a class="bandT" href="/account" aria-current="page">Account</a></li>
129
134
  <!-- the honest hover target; the delegated click upgrades it to the POST -->
130
135
  <li class="bandCell"><a class="bandT signout" href="/api/bongos/auth/logout">Sign out</a></li>
131
136
  </ul>
@@ -200,7 +205,8 @@
200
205
  spec D1/D3, task 1002966), PATCH /me/cross-project. Ported from the hall's
201
206
  old "Profile privacy" section. The recruiter copy must name EXACTLY what
202
207
  being discoverable grants (the D3 sliver) — tests/account_privacy_flags.mjs
203
- checks it against the module's own key lists. -->
208
+ checks it against the module's own key lists. Private shows the guild
209
+ notice under its choice (ADR 0337 D4.4, task 1004260). -->
204
210
  <section class="aSec" id="aPrivacy" aria-labelledby="aPrivacy-h">
205
211
  <h2 id="aPrivacy-h">who can find you</h2>
206
212
  <p class="aSide">Your profile page can be public or private, and recruiters are let in separately — so a private profile can still be reachable for work, and a public one can stay out of recruiting entirely.</p>
@@ -211,6 +217,7 @@
211
217
  <label class="aOpt"><input type="radio" name="visibility" value="public"><span><b>Public</b>Anyone can open your profile page and find you by name.</span></label>
212
218
  <label class="aOpt"><input type="radio" name="visibility" value="private"><span><b>Private</b>Private hides the page from everyone — including you: it answers “not found” to every visitor, and you read your own record here instead.</span></label>
213
219
  </div>
220
+ <div class="aGuilds" id="visGuilds" hidden><p class="aWhat" id="visGuildsWhat"></p><ul class="aGuildList" id="visGuildList"></ul></div>
214
221
  </div>
215
222
  <div class="aBand">
216
223
  <span class="aLabel" id="recLabel">Recruiters</span>
@@ -229,7 +236,8 @@
229
236
  <!-- YOUR RECORD — hide_stats (ADR 0171 D4, task 1002279), PATCH
230
237
  /me/cross-project. Ported from the hall's old "Cross-project record"
231
238
  section. Enforcement is in the module's read functions, so it applies at
232
- once. The owner always sees their own numbers here. -->
239
+ once. The owner always sees their own numbers here. Hide shows the guild
240
+ notice under its choice (ADR 0337 D4.4, task 1004260). -->
233
241
  <section class="aSec" id="aRecord" aria-labelledby="aRecord-h">
234
242
  <h2 id="aRecord-h">your record</h2>
235
243
  <p class="aSide">Your track record across every project you build on. Hiding it takes effect immediately everywhere — your profile, the recruiting directory, and a recruiter’s look-up of one builder.</p>
@@ -241,6 +249,7 @@
241
249
  <label class="aOpt"><input type="radio" name="record" value="show"><span><b>Show it</b>Your numbers appear on your profile and in the directories.</span></label>
242
250
  <label class="aOpt"><input type="radio" name="record" value="hide"><span><b>Hide it</b>Your numbers leave your profile and the directories. You always see them here.</span></label>
243
251
  </div>
252
+ <div class="aGuilds" id="recordGuilds" hidden><p class="aWhat" id="recordGuildsWhat"></p><ul class="aGuildList" id="recordGuildList"></ul></div>
244
253
  </div>
245
254
  <div class="actions"><button type="submit" class="btn" id="recordSave">save</button></div>
246
255
  <p class="projMsg" id="recordMsg" role="status" aria-live="polite"></p>
@@ -348,11 +357,12 @@
348
357
  var target = hash && document.getElementById(hash.slice(1));
349
358
  if (target && typeof target.scrollIntoView === 'function') target.scrollIntoView();
350
359
  loadBuilding();
360
+ loadGuilds();
351
361
  }, function () { showAlert('Couldn’t reach the server — check your connection and refresh.'); });
352
362
  }
353
363
 
354
364
  // ---- paint: every control from the server's answer ----
355
- function paint() { paintProfile(); paintHandle(); paintPrivacy(); paintRecord(); }
365
+ function paint() { paintProfile(); paintHandle(); paintPrivacy(); paintRecord(); paintGuilds(); }
356
366
 
357
367
  function paintProfile() {
358
368
  $('fName').value = acct.display_name || '';
@@ -527,6 +537,9 @@
527
537
  document.querySelectorAll('input[name="visibility"], input[name="recruiters"], input[name="record"]').forEach(function (r) {
528
538
  r.addEventListener('change', paintRecruitWhat);
529
539
  });
540
+ document.querySelectorAll('input[name="visibility"], input[name="record"]').forEach(function (r) {
541
+ r.addEventListener('change', paintGuilds);
542
+ });
530
543
  $('privacyForm').addEventListener('submit', function (ev) {
531
544
  ev.preventDefault();
532
545
  var msg = $('privacyMsg');
@@ -555,6 +568,53 @@
555
568
  : 'Cross-project record shown on your public profile.');
556
569
  });
557
570
 
571
+ // ---- the guilds that count your numbers (ADR 0337 D4.4, task 1004260).
572
+ // Every guild counts a member's numbers in its rounded totals unless they
573
+ // leave them out on that guild's own page — a private account and a hidden
574
+ // record included. So where this page offers private or hidden, it says so
575
+ // for the choice as it stands, before anything is saved, and lists the
576
+ // builder's guilds, each linking to the page where its switch lives. The
577
+ // switch itself is the guild page's; this page never draws or writes it.
578
+ // GET /me/guilds is read once, after the account: `guilds` stays null until
579
+ // it answers, and a failed read still says the sentence, with no list. ----
580
+ var guilds = null;
581
+ var guildsFailed = false;
582
+ function loadGuilds() {
583
+ return request('GET', '/me/guilds').then(function (res) {
584
+ if (res.ok && res.data && Array.isArray(res.data.guilds)) guilds = res.data.guilds;
585
+ else guildsFailed = true;
586
+ paintGuilds();
587
+ }, function () { guildsFailed = true; paintGuilds(); });
588
+ }
589
+ function paintGuilds() {
590
+ paintGuildNote('vis', picked('visibility') === 'private', 'your profile page is private');
591
+ paintGuildNote('record', picked('record') === 'hide', 'your record is hidden');
592
+ }
593
+ // In no guild, nothing to say; not read yet, nothing yet.
594
+ function paintGuildNote(key, applies, despite) {
595
+ var box = $(key + 'Guilds');
596
+ var known = !!(guilds && guilds.length);
597
+ box.hidden = !applies || !(known || guildsFailed);
598
+ if (box.hidden) return;
599
+ $(key + 'GuildsWhat').textContent = 'Every guild you belong to counts your numbers in its rounded totals unless you leave them out — even while ' + despite + '. ' +
600
+ 'They are never shown on their own: only inside totals rounded down to a band (10+, 20+, 50+ …) and counted once a week. ' +
601
+ (known ? 'Each guild’s page has the switch:' : 'Each guild’s page has the switch.');
602
+ var ul = $(key + 'GuildList');
603
+ ul.textContent = '';
604
+ (guilds || []).forEach(function (g) { ul.appendChild(guildItem(g)); });
605
+ }
606
+ function guildItem(g) {
607
+ var li = rowEl('li');
608
+ var a = document.createElement('a');
609
+ a.href = '/g/' + encodeURIComponent(g.slug);
610
+ a.textContent = g.name || g.slug;
611
+ li.appendChild(a);
612
+ var state = rowEl('span', 'aTag');
613
+ state.textContent = g.counted_in_totals ? ' · counted' : ' · left out';
614
+ li.appendChild(state);
615
+ return li;
616
+ }
617
+
558
618
  // ---- projects you build on (task 1004277; the rows and Leave came from
559
619
  // /projects?view=mine, task 1003774). Read once the account is shown, on its
560
620
  // own: a failed read says so and never paints a false "not building on
@@ -1,5 +1,5 @@
1
1
  {
2
- "_": "The account page's states for modules/ui-design/kit/render.js (task 1004259, ADR 0337 D4.3): signed out it is the sign-in door and nothing else; signed in it is the hub's bar (task 1004455: the identity chip from GET /me, the Projects band and the profile band with Account current) over the five sections, painted from the stub's one account (fixtures/me__cross-project.json — public, recruiters following the profile setting, a bio and two links), the fifth being the projects you build on, from fixtures/my-projects.json (task 1004277; in-building lands on it through /account#aBuilding). Dark only, like every apex page (ADR 0204). The private, hidden, not-yet-published account renders from account-private.states.json.",
2
+ "_": "The account page's states for modules/ui-design/kit/render.js (task 1004259, ADR 0337 D4.3): signed out it is the sign-in door and nothing else; signed in it is the hub's bar (task 1004455: the identity chip from GET /me, the Projects band and the profile band with Account current) over the five sections, painted from the stub's one account (fixtures/me__cross-project.json — public, recruiters following the profile setting, a bio and two links), the fifth being the projects you build on, from fixtures/my-projects.json (task 1004277; in-building lands on it through /account#aBuilding). The guild notice (task 1004260, ADR 0337 D4.4) stays hidden while the page is public and shown; in-privacy picks Private and it appears under that choice with the builder's two guilds from the stub's held set (fixtures/guilds.json via GET /me/guilds). Dark only, like every apex page (ADR 0204). The private, hidden, not-yet-published account renders from account-private.states.json.",
3
3
  "page": "/account",
4
4
  "surface": "public-landing",
5
5
  "modes": ["dark"],
@@ -9,11 +9,11 @@
9
9
  "ignoreControls": [".vh"],
10
10
  "states": {
11
11
  "out": { "auth": false, "actions": [["wait", 300]], "expect": { "visible": ["#door", "#signin", "#signinTop", "#projTrig"], "hidden": ["#acctBody", "#who", "#loading"] } },
12
- "in": { "auth": true, "actions": [["wait", 400]], "expect": { "visible": ["#acctBody", "#fName", "#fBio", "#fHandle", "#recWhat", "#recordLine", "#who"], "text": [["#whoLogin", "builder"], ["#recordLine", "Your record: 3 projects · 1000 credits · 19 works shipped · 10 karma."]], "hidden": ["#door", "#loading", "#signinTop", "#menuProjects", "#menuProfile"], "value": [["#fHandle", "builder"], ["#fName", "A Builder"], ["#fLinkX", "https://x.com/builder"]] } },
12
+ "in": { "auth": true, "actions": [["wait", 400]], "expect": { "visible": ["#acctBody", "#fName", "#fBio", "#fHandle", "#recWhat", "#recordLine", "#who"], "text": [["#whoLogin", "builder"], ["#recordLine", "Your record: 3 projects · 1000 credits · 19 works shipped · 10 karma."]], "hidden": ["#door", "#loading", "#signinTop", "#menuProjects", "#menuProfile", "#visGuilds", "#recordGuilds"], "value": [["#fHandle", "builder"], ["#fName", "A Builder"], ["#fLinkX", "https://x.com/builder"]] } },
13
13
  "in-projects-band": { "auth": true, "actions": [["wait", 400], ["click", "#projTrig"], ["wait", 300]], "expect": { "visible": ["#menuProjects", "#mineCell"], "hidden": ["#menuProfile"] } },
14
14
  "in-profile-band": { "auth": true, "actions": [["wait", 400], ["click", "#who"], ["wait", 300]], "expect": { "visible": ["#menuProfile"], "hidden": ["#menuProjects"], "text": [["#bandWhoLogin", "builder"], ["#menuProfile [aria-current=\"page\"]", "Account"]] } },
15
15
  "in-building": { "auth": true, "url": "/account#aBuilding", "actions": [["wait", 500]], "expect": { "visible": ["#aBuilding", "#buildingList"], "hidden": ["#buildingEmpty", "#buildingLoading"], "count": [["#buildingList .mineRow", 2]], "text": [["#buildingList .mineRow:first-child .mineRow__name", "Example World"], ["#buildingList .mineRow:first-child .mineRow__host", "example.com"]], "attr": [["#buildingList .mineRow:first-child button", "aria-label", "Leave Example World"]] } },
16
- "in-privacy": { "auth": true, "actions": [["wait", 400], ["click", "#aPrivacy .aOpt:nth-child(2)"], ["wait", 150]], "expect": { "checked": "input[name=visibility][value=private]", "text": [["#recWhat", "They will see your name, your picture, your roles, and whether you have been active in the last 30 days — never your credits, works shipped, karma, or the projects you build on. This is the one thing a recruiter can see while your profile page is private. Turning this off removes you from both recruiting surfaces — the recruiting directory, and a recruiter’s look-up of one builder — straight away. · For now this follows your profile page setting."]] } }
16
+ "in-privacy": { "auth": true, "actions": [["wait", 400], ["click", "#aPrivacy .aOpt:nth-child(2)"], ["wait", 150]], "expect": { "checked": "input[name=visibility][value=private]", "visible": ["#visGuilds"], "hidden": ["#recordGuilds"], "count": [["#visGuildList li", 2]], "attr": [["#visGuildList li:first-child a", "href", "/g/forge-crew"]], "text": [["#visGuildList li:first-child a", "Forge Crew"], ["#recWhat", "They will see your name, your picture, your roles, and whether you have been active in the last 30 days — never your credits, works shipped, karma, or the projects you build on. This is the one thing a recruiter can see while your profile page is private. Turning this off removes you from both recruiting surfaces — the recruiting directory, and a recruiter’s look-up of one builder — straight away. · For now this follows your profile page setting."]] } }
17
17
  },
18
18
  "reducedMotion": { "auth": true, "actions": [["wait", 300]] }
19
19
  }
@@ -8,5 +8,5 @@
8
8
  "prompt": "Product render on a pure black background: a pair of bongo drums, one larger and one smaller joined by a short bridge, their shells liquid chrome with iridescent bronze, blue and violet reflections, crisp clean silhouette, seen straight on from slightly below so the drumheads are thin ellipses, faintly translucent drumheads, no hands, no text, no ground plane, no cast shadow or glow on the background. Pure black everywhere the drums are not.",
9
9
  "source": ".impeccable/mocks/world-assets/r2-bongos-chrome-b.jpg",
10
10
  "encoded": "verbatim copy of the plate (task 1003298)",
11
- "use": "dark mode: black plate composited with mix-blend-mode lighten"
11
+ "use": "the verbatim plate the .webp is encoded from, kept so a catalog art_url naming it still resolves; the page requests the .webp (task 1003649)"
12
12
  }
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "object",
3
+ "model": "nano_banana_pro",
4
+ "aspect_ratio": "1:1",
5
+ "resolution": "2k",
6
+ "generated": "2026-08-27T13:24Z",
7
+ "round": 2,
8
+ "task": 1003649,
9
+ "prompt": "Product render on a pure black background: a pair of bongo drums, one larger and one smaller joined by a short bridge, their shells liquid chrome with iridescent bronze, blue and violet reflections, crisp clean silhouette, seen straight on from slightly below so the drumheads are thin ellipses, faintly translucent drumheads, no hands, no text, no ground plane, no cast shadow or glow on the background. Pure black everywhere the drums are not.",
10
+ "source": ".impeccable/mocks/world-assets/r2-bongos-chrome-b.jpg",
11
+ "encoded": "ffmpeg libwebp quality 88, yuv420p, full resolution, black ground kept at the source JPEG level (task 1003649)",
12
+ "use": "dark mode: black plate composited with mix-blend-mode lighten"
13
+ }
@@ -8,5 +8,5 @@
8
8
  "prompt": "A single iridescent liquid-chrome blob, a glossy organic form with a crisp clean edge and a mirror-smooth surface reflecting bronze, deep blue and violet with one thin bright rim highlight, centred on a pure black background, nothing else, no text, no ground plane, no shadow or glow on the background. Pure black everywhere the blob is not.",
9
9
  "source": ".impeccable/mocks/world-assets/r2-chrome-blob-one.jpg",
10
10
  "encoded": "verbatim copy of the plate (task 1003298)",
11
- "use": "dark mode: black plate composited with mix-blend-mode lighten"
11
+ "use": "the verbatim plate the .webp is encoded from, kept so a catalog art_url naming it still resolves; the page requests the .webp (task 1003649)"
12
12
  }
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "object",
3
+ "model": "nano_banana_pro",
4
+ "aspect_ratio": "1:1",
5
+ "resolution": "2k",
6
+ "generated": "2026-08-27T13:24Z",
7
+ "round": 2,
8
+ "task": 1003649,
9
+ "prompt": "A single iridescent liquid-chrome blob, a glossy organic form with a crisp clean edge and a mirror-smooth surface reflecting bronze, deep blue and violet with one thin bright rim highlight, centred on a pure black background, nothing else, no text, no ground plane, no shadow or glow on the background. Pure black everywhere the blob is not.",
10
+ "source": ".impeccable/mocks/world-assets/r2-chrome-blob-one.jpg",
11
+ "encoded": "ffmpeg libwebp quality 88, yuv420p, full resolution, black ground kept at the source JPEG level (task 1003649)",
12
+ "use": "dark mode: black plate composited with mix-blend-mode lighten"
13
+ }
@@ -8,5 +8,5 @@
8
8
  "prompt": "Full-frame render on a pure black background: three iridescent liquid-chrome blobs, glossy organic forms with crisp clean edges and mirror-smooth surfaces reflecting bronze, deep blue and violet, thin bright rim highlights, strong depth: one very large blob upper centre-right, a medium blob lower left, a small blob far right, generous empty black space between them, nothing else, no text, no ground plane, no shadows or glow on the background. Pure black everywhere the blobs are not, so the image can be composited with a lighten blend.",
9
9
  "source": ".impeccable/mocks/world-assets/r2-chrome-blobs-set.jpg",
10
10
  "encoded": "verbatim copy of the plate (task 1003298)",
11
- "use": "dark mode: black plate composited with mix-blend-mode lighten"
11
+ "use": "the verbatim plate the .webp is encoded from, kept so a catalog art_url naming it still resolves; the page requests the .webp (task 1003649)"
12
12
  }
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "plate",
3
+ "model": "nano_banana_pro",
4
+ "aspect_ratio": "16:9",
5
+ "resolution": "2k",
6
+ "generated": "2026-08-27T13:24Z",
7
+ "round": 2,
8
+ "task": 1003649,
9
+ "prompt": "Full-frame render on a pure black background: three iridescent liquid-chrome blobs, glossy organic forms with crisp clean edges and mirror-smooth surfaces reflecting bronze, deep blue and violet, thin bright rim highlights, strong depth: one very large blob upper centre-right, a medium blob lower left, a small blob far right, generous empty black space between them, nothing else, no text, no ground plane, no shadows or glow on the background. Pure black everywhere the blobs are not, so the image can be composited with a lighten blend.",
10
+ "source": ".impeccable/mocks/world-assets/r2-chrome-blobs-set.jpg",
11
+ "encoded": "ffmpeg libwebp quality 88, yuv420p, full resolution, black ground kept at the source JPEG level (task 1003649)",
12
+ "use": "dark mode: black plate composited with mix-blend-mode lighten"
13
+ }