@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
@@ -43,9 +43,10 @@
43
43
  // by anyone reading the log or grepping the table.
44
44
  // - Tasks return to status='ready', no credits, just like a manual release.
45
45
  // - Affected builders get one broadcast each via scripts/discord/post.js
46
- // (Discord #ship-news — outbound moved off Google Chat per ADR 0032 /
47
- // #607; scripts/chat/ is inbound-only now). Post failure is non-fatal
48
- // (mirrors ship.js).
46
+ // (Discord #ship-feed, the builders' channel — task 1002846 keeps the
47
+ // owner-facing #ship-news for shipped features and fixes; outbound moved
48
+ // off Google Chat per ADR 0032 / #607; scripts/chat/ is inbound-only now).
49
+ // Post failure is non-fatal (mirrors ship.js).
49
50
  // - Exit codes: 0 = success (including "nothing stale"); 1 = fatal error.
50
51
 
51
52
  const fs = require('node:fs');
@@ -106,11 +107,13 @@ function parseThresholdHours(args) {
106
107
 
107
108
  // Post a single broadcast about an auto-released claim. Best-effort —
108
109
  // returns { ok, error } and never throws. Mirrors the pattern in ship.js
109
- // (synchronous execFile via the local scripts/discord/post.js helper —
110
- // default channel ship_news; outbound moved off Google Chat per ADR 0032).
110
+ // (synchronous execFile via the local scripts/discord/post.js helper, on
111
+ // #ship-feed: a reclaim nudge is builder housekeeping, never owner news —
112
+ // task 1002846; outbound moved off Google Chat per ADR 0032). The line opens
113
+ // with `@name`, never `#<id>`, so the applause bot ignores it.
111
114
  function postDiscordPing(text) {
112
115
  try {
113
- execFileSync('node', ['scripts/discord/post.js', text], {
116
+ execFileSync('node', ['scripts/discord/post.js', '--channel', 'ship_feed', text], {
114
117
  cwd: REPO_ROOT,
115
118
  stdio: 'pipe',
116
119
  });
@@ -35,6 +35,7 @@
35
35
  // that gets muted, and this one is aimed at the surface a person actually reads.
36
36
 
37
37
  const NO_ARTIFACT_SOURCE = 'no-artifact-close';
38
+ const { postWithAsk } = require('./ask.js');
38
39
 
39
40
  // One blocker per task. Deliberately NOT keyed on the builder or the run: two builders
40
41
  // hitting the same stuck task should land on the same blocker, not race to file two.
@@ -85,6 +86,22 @@ function escalationBody({ taskId, title, floor, summary, notes, builder }) {
85
86
  return lines.join('\n');
86
87
  }
87
88
 
89
+ // The card the archon answers (task 1004704): the escalation as an ask, so the
90
+ // review queue shows one short question instead of the body above. The body
91
+ // still carries everything; the card is what a person answers at a glance.
92
+ function escalationAsk(taskId) {
93
+ return {
94
+ question: `Close task ${taskId} with no code to grade?`,
95
+ options: [
96
+ { key: 'close', label: 'Close it', result: 'It closes as done, without a grade.', act: 'go ahead' },
97
+ { key: 'return', label: 'Send it back', result: 'The claim is released and the task returns to ready.', act: 'need more' },
98
+ ],
99
+ recommend: { key: 'close', why: 'The builder says no code was needed; read their reason first.' },
100
+ waits_on: 'lead:release',
101
+ if_nobody_answers: 'The task stays at completed and its builder is not paid.',
102
+ };
103
+ }
104
+
88
105
  // The terminating refusal. THE ONE INVARIANT: these lines must never send the builder to
89
106
  // --regrade, because --regrade on an empty diff sends them straight back to --allow-empty.
90
107
  // That mutual advice IS the bug (task 1003950). Pure + exported so a test can assert the
@@ -108,7 +125,8 @@ function noArtifactRefusalLines({ taskId, floor, escalation }) {
108
125
  // Fail-open: the close is still blocked, so the builder must not be told it worked.
109
126
  const why = escalation && escalation.reason ? ` (${escalation.reason})` : '';
110
127
  lines.push(`Could not file the escalation automatically${why}.`);
111
- lines.push('File it by hand so this does not sit silently — post a blocker naming this task:');
128
+ lines.push('File it by hand so this does not sit silently — post a blocker naming this task,');
129
+ lines.push('with an ask (check the card first: node scripts/gds/ask.js check <file>):');
112
130
  lines.push(' node scripts/gds/api.js POST /blockers --body-file <path-to-json>');
113
131
  }
114
132
  lines.push('');
@@ -124,12 +142,12 @@ async function fileNoArtifactEscalation({ taskId, title, floor, summary, notes,
124
142
  const post = call || require('./cli-lib').apiCall;
125
143
  let r;
126
144
  try {
127
- r = await post('POST', '/api/bongos/blockers', {
145
+ r = await postWithAsk((body) => post('POST', '/api/bongos/blockers', body), {
128
146
  title: escalationTitle(taskId, title),
129
147
  body_md: escalationBody({ taskId, title, floor, summary, notes, builder }),
130
148
  source: NO_ARTIFACT_SOURCE,
131
149
  source_ref: escalationSourceRef(taskId),
132
- });
150
+ }, escalationAsk(taskId));
133
151
  } catch (err) {
134
152
  return { filed: false, reason: `POST /blockers threw: ${err && err.message}` };
135
153
  }
@@ -143,6 +161,7 @@ module.exports = {
143
161
  escalationSourceRef,
144
162
  escalationTitle,
145
163
  escalationBody,
164
+ escalationAsk,
146
165
  noArtifactRefusalLines,
147
166
  fileNoArtifactEscalation,
148
167
  };
@@ -96,10 +96,11 @@ async function mergeAndDeploy({ taskId, summary, advanceToMerge, skipMerge, dbOn
96
96
  // to fire IN ADDITION to the server's, so a single ship announced FOUR
97
97
  // messages (local #ship-news + #ship-feed, then the server's #ship-news +
98
98
  // #ship-feed). Defer unconditionally so exactly one broadcast fires — from
99
- // the server, after the deploy lands. (#ship-news itself is paused in
100
- // chooseBroadcastTargets, so that one broadcast is the #ship-feed line —
101
- // task 1749 / idea 458.) The security-sensitive fail-closed routing lives
102
- // server-side in ship-broadcast.postShipBroadcast, so it is preserved.
99
+ // the server, after the deploy lands. (That one broadcast is the #ship-feed
100
+ // line, plus a plain #ship-news headline for a feature or a bug — the kind
101
+ // filter in chooseBroadcastTargets, task 1002846.) The security-sensitive
102
+ // fail-closed routing lives server-side in ship-broadcast.postShipBroadcast,
103
+ // so it is preserved.
103
104
  console.log(' Discord broadcast: deferred to the server — it posts #ship-feed after the deploy lands.');
104
105
  }
105
106
  return merged;
@@ -176,19 +176,31 @@ function removeProbe(repoRoot, dir) {
176
176
  // (task 1004584). It only stands down when the pre-merge base lacks the file
177
177
  // too, so a branch that DELETES fitness.js from the core is still measured
178
178
  // (and blocked as unmeasurable) rather than waved through.
179
- function gateApplies(repoRoot, baseSha) {
180
- if (fs.existsSync(path.join(repoRoot, 'scripts', 'gds', 'fitness.js'))) return true;
181
- if (!baseSha) return true;
179
+ //
180
+ // It asks GIT, never the disk (task 1004728). A vendored instance keeps an
181
+ // untracked `scripts -> node_modules/@bongos/core/scripts` link in its main
182
+ // checkout (hidden by .git/info/exclude; grading needs it), so the core's own
183
+ // fitness.js IS on disk there. The disk check read that as "this repo carries
184
+ // the gate", ran the core's fitness against a tree with no core in it, and
185
+ // blocked every land on toast. What the repo carries is what it tracks.
186
+ function tracksFitness(repoRoot, rev) {
182
187
  try {
183
- execFileSync('git', ['cat-file', '-e', `${baseSha}:scripts/gds/fitness.js`], {
188
+ execFileSync('git', ['cat-file', '-e', `${rev}:scripts/gds/fitness.js`], {
184
189
  cwd: repoRoot, stdio: 'ignore', shell: false,
185
190
  });
186
- return true; // the base had it — this merge removed it; measure (and block)
191
+ return true;
187
192
  } catch (_) {
188
- return false; // neither side carries the gate — nothing to run
193
+ return false;
189
194
  }
190
195
  }
191
196
 
197
+ function gateApplies(repoRoot, baseSha) {
198
+ if (tracksFitness(repoRoot, 'HEAD')) return true;
199
+ if (!baseSha) return true;
200
+ // the base had it — this merge removed it; measure (and block)
201
+ return tracksFitness(repoRoot, baseSha);
202
+ }
203
+
192
204
  function gateMergeResult({ repoRoot, baseSha, log = console.log }) {
193
205
  if (!gateApplies(repoRoot, baseSha)) {
194
206
  log(' fitness: this repo carries no scripts/gds/fitness.js (a vendored instance) — gate not applicable');
@@ -23,7 +23,13 @@ node scripts/hall-preview/server.js # neutral pack, port 4600
23
23
  | `--empty` | drain every array in every fixture — the empty-state screenshot pass |
24
24
  | `--fixture-me` | don't read your own account live — serve the checked-in `me.json` snapshot (see below) |
25
25
 
26
- Writes are not wired: any `POST/PATCH/DELETE` answers `501 preview_readonly`.
26
+ Writes are not wired: any `POST/PATCH/DELETE` answers `501 preview_readonly` — except
27
+ the canned writes in `server.js` and, since task 1004685, the five per-builder pref
28
+ PATCHes (`/me/interaction|wandering|model-allocation|render|skill-prefs`), which
29
+ [`pref-writes.js`](pref-writes.js) answers with the real shapers over an in-memory copy
30
+ and lays over later reads (logged as `recorded: PATCH …`; a restart forgets them).
31
+ `?as=nobody` on any hall page views the hall as a builder who holds no governing seat
32
+ (the living-city prototype; session-sticky, `?as=me` clears it).
27
33
  A GET with no fixture answers 404 and logs
28
34
  `no fixture: <path>` — that log line names the exact file to add.
29
35
 
@@ -91,6 +97,40 @@ node scripts/hall-preview/refresh-fixtures.js --scrub-only
91
97
  `tests/hall_preview_scrub.mjs` fails while any committed fixture still carries a real
92
98
  identity.
93
99
 
100
+ Two more things a run does (task 1004685). It **keeps each builder's number**: the
101
+ committed `builders__roster.json` is joined to the live roster by builder id, so
102
+ `builder-12` is still `builder-12` after a refresh and a new fixture lines up with the
103
+ old ones. And it ends with a **prose pass**: every person the run met in a field is
104
+ replaced wherever a sentence names them too (by login, display name, the first name a
105
+ login starts with, or a nickname cut from an all-caps login), because the governor
106
+ docket's titles and details name people outright. The pass only knows people a field
107
+ named, so a first name nobody's field carries (a person mentioned only in prose) can
108
+ still slip through — read the governor fixtures' diff before committing them.
109
+
110
+ **No security content is ever checked in either** (task 1004685, after its security
111
+ review). The governor docket is read at a rank floor and carries the open red-team
112
+ reports and the owner's blockers, so every run — and `--scrub-only` — ends with
113
+ `createSecurityRedactor` (`scrub-identity.js`): a red-team record keeps only its id,
114
+ state, severity bucket and acts under the title "Security report — details withheld";
115
+ a blocker keeps no body and no title that names a secret, a host step or a customer
116
+ project; the security backlog's tasks (flagged `security_sensitive`, or in a goal of
117
+ category `security`) keep their place but not their words; any prose naming a
118
+ credential, host config or the code a live report points at is emptied; and a
119
+ customer instance's own domain becomes `instance.example`.
120
+ `tests/hall_preview_security.mjs` fails while any committed fixture is not already a
121
+ fixed point of that pass.
122
+
123
+ **The governor reads are live too.** `government/docket`, `constitution`,
124
+ `board/items`, `board/genesis-stages`, `goals/rollups`, `blockers`, `autonomy/fence`,
125
+ `moving`, `sessions/search`, `learnings` (rows only) and the viewer's
126
+ `me/{interaction,wandering,model-allocation}` are in the snapshot list. `/tasks` is the
127
+ one path read two ways: with no query it is the ready list (`tasks.json`), with
128
+ `?status=active` it is the work being built (`tasks__status-active.json`); the server's
129
+ `QUERY_FIXTURES` table picks between them. The government's own reads (`government/laws`,
130
+ `leads`, `treasury`, `presets`, `counts`) are fixtures in the same shape the routes
131
+ answer; the living city and street run on them alone (task 1004717 deleted the prototype
132
+ overlay). A concept no table holds is not drawn: never add sample data for one.
133
+
94
134
  ## The evidence matrix (per page-redesign ship)
95
135
 
96
136
  Screenshot every changed page at **{light, dark} × {1280w, 390w}**, plus one
@@ -0,0 +1,140 @@
1
+ // scripts/hall-preview/pref-writes.js — the harness's SETTINGS RECORDER (task
2
+ // 1004685, the living-city prototype's My agents page).
3
+ //
4
+ // WHY. The harness has no database, so every write answers 501 or one fixed canned
5
+ // fixture. My agents' whole job is changing your own agent settings, and a page
6
+ // that can only ever show "not saved" cannot be reviewed by looking (the trap
7
+ // modules/hall-ui/CLAUDE.md names). So the five per-builder pref PATCH routes are
8
+ // answered HERE, by the same pure shapers the real routes call
9
+ // (modules/builder-settings/routes/settings.js: validatePatch → applyPatch →
10
+ // describeForApi), over an in-memory copy of the served builder's prefs. A 422 is
11
+ // the real route's 422; a 200 is the body the real route would send, contract
12
+ // text included. The recorded state is then laid over GET /me and GET /me/<slice>,
13
+ // so a reload shows the change too.
14
+ //
15
+ // What it is NOT: storage. Nothing is written to disk and a restart forgets
16
+ // everything; each recorded write is logged so the harness console shows what the
17
+ // page sent. Only these five routes are recorded — every other write is refused
18
+ // exactly as before.
19
+ 'use strict';
20
+
21
+ const path = require('path');
22
+
23
+ const ROOT = path.resolve(__dirname, '..', '..');
24
+ const lib = (name) => require(path.join(ROOT, 'modules', 'builder-settings', name));
25
+ const interaction = lib('interaction-prefs');
26
+ const wandering = lib('wandering-prefs');
27
+ const models = lib('model-alloc-prefs');
28
+ const render = lib('render-prefs');
29
+ const skills = lib('skill-prefs');
30
+
31
+ // apiPath → the /me slice it answers for (null: the skill prefs, not on /me)
32
+ const ROUTES = {
33
+ 'me/interaction': 'interaction',
34
+ 'me/wandering': 'wandering',
35
+ 'me/model-allocation': 'model_allocation',
36
+ 'me/render': 'render',
37
+ 'me/skill-prefs': null,
38
+ };
39
+
40
+ function createPrefRecorder({ log = console.log } = {}) {
41
+ let behavior = null; // the behavior_prefs jsonb, as the routes see it
42
+ let skillPrefs = null; // the skill_model_prefs jsonb
43
+ let rank = null;
44
+ let instanceModels = {};
45
+ const touched = new Set();
46
+
47
+ // Rebuild the stored blobs from what the served /me says, once.
48
+ function seed(me, skillFixture) {
49
+ if (behavior) return;
50
+ const m = me || {};
51
+ behavior = {};
52
+ if (m.interaction && m.interaction.stored) behavior[interaction.RESERVED_KEY] = { ...m.interaction.stored };
53
+ if (m.wandering && m.wandering.override_level != null) behavior[wandering.RESERVED_KEY] = m.wandering.override_level;
54
+ if (m.model_allocation && m.model_allocation.override) behavior[models.RESERVED_KEY] = { ...m.model_allocation.override };
55
+ if (m.render && m.render.override_level) behavior[render.RESERVED_KEY] = m.render.override_level;
56
+ rank = (m.builder && m.builder.rank) || (m.wandering && m.wandering.rank) || null;
57
+ instanceModels = (m.model_allocation && m.model_allocation.instance_default) || {};
58
+ skillPrefs = { ...((skillFixture && skillFixture.prefs) || {}) };
59
+ }
60
+
61
+ function describe(slice) {
62
+ if (slice === 'interaction') return interaction.describeForApi(behavior);
63
+ if (slice === 'wandering') return wandering.describeForApi(behavior, rank);
64
+ if (slice === 'model_allocation') return models.describeForApi(behavior, instanceModels);
65
+ if (slice === 'render') return render.describeForApi(behavior);
66
+ return {
67
+ prefs: skillPrefs, effective: skills.listEffective(skillPrefs), defaults: skills.SKILL_DEFAULTS,
68
+ known_skills: skills.KNOWN_SKILLS, allowed_models: skills.ALLOWED_MODELS,
69
+ };
70
+ }
71
+
72
+ function handles(method, apiPath) {
73
+ return method === 'PATCH' && Object.prototype.hasOwnProperty.call(ROUTES, apiPath);
74
+ }
75
+
76
+ // → { status, body }, the real route's answer
77
+ function write(apiPath, body, { me, skillFixture } = {}) {
78
+ seed(me, skillFixture);
79
+ const slice = ROUTES[apiPath];
80
+ const b = body || {};
81
+ const invalid = (errors) => ({ status: 422, body: { error: { code: 'invalid_patch', message: 'invalid_patch', details: { errors } } } });
82
+ let v;
83
+ if (slice === 'interaction') {
84
+ v = interaction.validatePatch(b);
85
+ if (!v.ok) return invalid(v.errors);
86
+ behavior = { ...behavior, [interaction.RESERVED_KEY]: interaction.applyPatch(interaction.storedProfile(behavior), v.patch) };
87
+ } else if (slice === 'wandering') {
88
+ v = wandering.validatePatch(b, { maxLevel: wandering.maxLevelForRank(rank) });
89
+ if (!v.ok) return invalid(v.errors);
90
+ behavior = wandering.applyPatch(behavior, v.patch);
91
+ } else if (slice === 'model_allocation') {
92
+ v = models.validatePatch(b);
93
+ if (!v.ok) return invalid(v.errors);
94
+ behavior = { ...behavior, [models.RESERVED_KEY]: models.applyPatch(models.storedOverride(behavior), v.patch) };
95
+ } else if (slice === 'render') {
96
+ v = render.validatePatch(b);
97
+ if (!v.ok) return invalid(v.errors);
98
+ behavior = render.applyPatch(behavior, v.patch);
99
+ } else {
100
+ v = skills.validatePatch(b);
101
+ if (!v.ok) return invalid(v.errors);
102
+ skillPrefs = skills.applyPatch(skillPrefs, v.patch);
103
+ }
104
+ touched.add(slice === null ? 'skills' : slice);
105
+ log(` recorded: PATCH ${apiPath} ${JSON.stringify(b)}`);
106
+ return { status: 200, body: describe(slice) };
107
+ }
108
+
109
+ // Lay the recorded state over a GET's payload; untouched reads pass through.
110
+ function overlay(apiPath, payload) {
111
+ if (!behavior || !payload || typeof payload !== 'object') return payload;
112
+ if (apiPath === 'me') {
113
+ const out = { ...payload };
114
+ // the render level is DERIVED from the interaction profile, so it moves with it
115
+ for (const s of ['interaction', 'wandering', 'model_allocation', 'render']) {
116
+ if (touched.has(s) || (s === 'render' && touched.has('interaction'))) out[s] = describe(s);
117
+ }
118
+ return out;
119
+ }
120
+ const slice = ROUTES[apiPath];
121
+ if (slice === undefined) return payload;
122
+ const key = slice === null ? 'skills' : slice;
123
+ return touched.has(key) || (key === 'render' && touched.has('interaction')) ? describe(slice) : payload;
124
+ }
125
+
126
+ return { handles, write, overlay };
127
+ }
128
+
129
+ // Read a JSON body off the raw request (the harness mounts no body parser).
130
+ function readJsonBody(req) {
131
+ return new Promise((resolve) => {
132
+ let raw = '';
133
+ req.setEncoding('utf8');
134
+ req.on('data', (c) => { raw += c; if (raw.length > 65536) req.destroy(); });
135
+ req.on('end', () => { try { resolve(raw ? JSON.parse(raw) : {}); } catch (_) { resolve(null); } });
136
+ req.on('error', () => resolve(null));
137
+ });
138
+ }
139
+
140
+ module.exports = { createPrefRecorder, readJsonBody, ROUTES };
@@ -12,13 +12,18 @@
12
12
  // snapshot goes through ./scrub-identity.js before it touches the disk: logins,
13
13
  // names, avatars, GitHub ids and emails become stand-ins (`builder-3` / `Builder
14
14
  // 3`), and the snapshotting account's own balance and spend become fixed values.
15
+ // Since task 1004685 a run also (1) keeps the numbers the committed roster already
16
+ // gives each builder, and (2) ends with a pass over the PROSE, replacing every person
17
+ // the run met in a field wherever a sentence names them (the governor docket does).
15
18
  // Hand-edited a fixture? Re-run the same pass over the files on disk, no network:
16
19
  // node scripts/hall-preview/refresh-fixtures.js --scrub-only
17
20
  'use strict';
18
21
 
19
22
  const fs = require('fs');
20
23
  const path = require('path');
21
- const { createScrubber, isOwnAccountFixture, highestStandIn, rewriteScalars } = require('./scrub-identity');
24
+ const {
25
+ createScrubber, createTextScrubber, securityRedactorFor, isOwnAccountFixture, highestStandIn, rewriteScalars,
26
+ } = require('./scrub-identity');
22
27
 
23
28
  const FIXTURES = path.join(__dirname, 'fixtures');
24
29
 
@@ -29,6 +34,16 @@ const trimList = (key, n) => (d) => (Array.isArray(d?.[key]) ? { ...d, [key]: d[
29
34
  // profile charts' pager back for the same page until "total". Pin total to
30
35
  // what the snapshot holds.
31
36
  const selfPaged = (d) => (Array.isArray(d?.rows) && d.page ? { ...d, page: { ...d.page, total: d.rows.length } } : d);
37
+ // The living city counts learnings and dates them; it never shows a body, and the
38
+ // bodies are long prose about the host (task 1004685). Keep the row, drop the essay.
39
+ const trimLearnings = (d) => (Array.isArray(d?.learnings)
40
+ ? { ...d, learnings: d.learnings.map(({ id, title, tags, task_id, created_at }) => ({ id, title, tags, task_id, created_at })) }
41
+ : d);
42
+ // The active list is read for WHERE work is moving (goal, module, discipline), not
43
+ // for what each task says; a description is the task's own long prose.
44
+ const trimTaskBodies = (d) => (Array.isArray(d?.tasks)
45
+ ? { ...d, tasks: d.tasks.map(({ description, value_summary, ...rest }) => rest) }
46
+ : d);
32
47
  const SNAPSHOTS = [
33
48
  ['me', 'me'],
34
49
  ['me/credits?limit=200', 'me__credits', selfPaged],
@@ -88,6 +103,40 @@ const SNAPSHOTS = [
88
103
  // draw them) no timeline. `versions` above is the AUTHENTICATED list; this is
89
104
  // the public one the page actually calls, and the two are not interchangeable.
90
105
  ['public/versions', 'public__versions'],
106
+
107
+ // THE GOVERNOR'S READS (task 1004685). The city and the street drew from
108
+ // hand-generated governor fixtures (tasks 1004524-1004580), so the harness only
109
+ // ever showed the reference design's sample council. These are the reads the
110
+ // pages make (city.js / street.js FEEDS), plus the ones the living city's model
111
+ // (modules/hall-ui/public/gov-living.js) takes: who is moving, the open blockers,
112
+ // the unattended-building switch, the sessions, the viewer's own agent settings.
113
+ ['government/docket', 'government__docket'],
114
+ // the counts every surface prints, in one read (task 1004716)
115
+ ['government/counts', 'government__counts'],
116
+ ['government/constitution', 'government__constitution'],
117
+ // the project's laws in force (task 1004706)
118
+ ['government/laws', 'government__laws'],
119
+ // who leads each building (task 1004709): the hub has seated no Lead yet
120
+ ['government/leads', 'government__leads'],
121
+ // the pots each goal draws from, the reserve, the spend cap (task 1004712)
122
+ ['government/treasury', 'government__treasury'],
123
+ // the strictness presets, and which one the laws in force amount to (task 1004715)
124
+ ['government/presets', 'government__presets'],
125
+ ['government/board/items', 'government__board__items'],
126
+ ['government/board/genesis-stages', 'government__board__genesis-stages'],
127
+ ['goals/rollups', 'goals__rollups'],
128
+ ['blockers?limit=100', 'blockers'],
129
+ ['autonomy/fence', 'autonomy__fence'],
130
+ ['moving', 'moving'],
131
+ // the work being built, with its goal; served for /tasks?status=active only
132
+ // (server.js QUERY_FIXTURES), since /tasks with no query is the ready list above
133
+ ['tasks?status=active&limit=200', 'tasks__status-active', trimTaskBodies],
134
+ ['sessions/search?limit=100', 'sessions__search'],
135
+ // what the project has learned: the city counts it, and no page reads the bodies
136
+ ['learnings', 'learnings', trimLearnings],
137
+ ['me/interaction', 'me__interaction'],
138
+ ['me/wandering', 'me__wandering'],
139
+ ['me/model-allocation', 'me__model-allocation'],
91
140
  ];
92
141
 
93
142
  // Every existing fixture's text — read so a run can number new stand-ins above the
@@ -101,13 +150,34 @@ function readFixtureTexts(dir) {
101
150
  }
102
151
  }
103
152
 
153
+ // continuitySeed — the people the committed roster already numbers, keyed by the
154
+ // builder id the live roster and the fixture share, so a re-run keeps builder-12 as
155
+ // builder-12 instead of renumbering every person in the snapshot (task 1004685). No
156
+ // roster on disk, or no live one, is an empty seed: the run numbers from scratch.
157
+ async function continuitySeed(api, dir) {
158
+ let onDisk;
159
+ try {
160
+ onDisk = JSON.parse(fs.readFileSync(path.join(dir, 'builders__roster.json'), 'utf8'));
161
+ } catch (_) { return []; }
162
+ const res = await api.request('GET', '/builders/roster', {});
163
+ if (!res || !res.ok || !Array.isArray(res.data?.builders) || !Array.isArray(onDisk?.builders)) return [];
164
+ const standIn = new Map();
165
+ for (const b of onDisk.builders) {
166
+ const m = /^builder-(\d+)$/i.exec(String(b?.github_login || ''));
167
+ if (m && b.id != null) standIn.set(String(b.id), Number(m[1]));
168
+ }
169
+ return res.data.builders
170
+ .filter((b) => b && b.id != null && standIn.has(String(b.id)))
171
+ .map((b) => ({ login: b.github_login, name: b.display_name, n: standIn.get(String(b.id)) }));
172
+ }
173
+
104
174
  // refreshFixtures — GET each snapshot, scrub the people out of it, write it. The api,
105
175
  // directory and logger are injected so tests/hall_preview_scrub.mjs drives this exact
106
176
  // writer with no network and a temp directory.
107
177
  async function refreshFixtures({ api, dir = FIXTURES, snapshots = SNAPSHOTS, log = console.log } = {}) {
108
178
  fs.mkdirSync(dir, { recursive: true });
109
- const scrubber = createScrubber({ firstNumber: highestStandIn(readFixtureTexts(dir)) + 1 });
110
- let wrote = 0;
179
+ const scrubber = createScrubber({ firstNumber: highestStandIn(readFixtureTexts(dir)) + 1, seed: await continuitySeed(api, dir) });
180
+ const fetched = [];
111
181
  for (const [apiPath, name, trim] of snapshots) {
112
182
  const res = await api.request('GET', `/${apiPath}`, {});
113
183
  if (!res.ok) {
@@ -116,8 +186,18 @@ async function refreshFixtures({ api, dir = FIXTURES, snapshots = SNAPSHOTS, log
116
186
  }
117
187
  // Scrubbed BEFORE the write, never after: fixtures/ is committed, so a copy of
118
188
  // the live response that touched the disk would already be one `git add` away.
119
- const data = scrubber.scrub(trim ? trim(res.data) : res.data, { ownAccount: isOwnAccountFixture(name) });
120
- fs.writeFileSync(path.join(dir, `${name}.json`), `${JSON.stringify(data, null, 2)}\n`);
189
+ fetched.push({ apiPath, name, data: scrubber.scrub(trim ? trim(res.data) : res.data, { ownAccount: isOwnAccountFixture(name) }) });
190
+ }
191
+ // Prose last (task 1004685): every snapshot's structure has taught the registry its
192
+ // people by now, so a name met in file 30 is still found in file 2's sentences.
193
+ const prose = createTextScrubber(scrubber.knownPeople());
194
+ const named = fetched.map((f) => ({ ...f, data: prose(f.data) }));
195
+ // Security content last of all (task 1004685): red-team write-ups, blocker
196
+ // runbooks, credentials and customer instance domains never reach the disk.
197
+ const secure = securityRedactorFor(named.map((f) => f.data));
198
+ let wrote = 0;
199
+ for (const { apiPath, name, data } of named) {
200
+ fs.writeFileSync(path.join(dir, `${name}.json`), `${JSON.stringify(secure(data), null, 2)}\n`);
121
201
  wrote += 1;
122
202
  log(` wrote ${name}.json (${apiPath})`);
123
203
  }
@@ -129,14 +209,28 @@ async function refreshFixtures({ api, dir = FIXTURES, snapshots = SNAPSHOTS, log
129
209
  // hand-written ones (several copied real people in by hand) and anything written
130
210
  // before the scrub existed. Only values that change are rewritten, so a fixture
131
211
  // someone formatted by hand keeps its formatting.
212
+ // Whether two JSON values have the same keys all the way down (values may differ).
213
+ function sameKeys(a, b) {
214
+ if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((x, i) => sameKeys(x, b[i]));
215
+ if (a && typeof a === 'object' && b && typeof b === 'object') {
216
+ const ka = Object.keys(a);
217
+ return ka.length === Object.keys(b).length && ka.every((k) => k in b && sameKeys(a[k], b[k]));
218
+ }
219
+ return (a && typeof a === 'object') === (b && typeof b === 'object');
220
+ }
221
+
132
222
  function scrubFixturesOnDisk({ dir = FIXTURES, log = console.log } = {}) {
133
223
  const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
134
224
  const texts = files.map((f) => fs.readFileSync(path.join(dir, f), 'utf8'));
135
225
  const scrubber = createScrubber({ firstNumber: highestStandIn(texts) + 1 });
226
+ const parsed = texts.map((t) => JSON.parse(t));
227
+ const secure = securityRedactorFor(parsed);
136
228
  let changed = 0;
137
229
  files.forEach((file, idx) => {
138
- const clean = scrubber.scrub(JSON.parse(texts[idx]), { ownAccount: isOwnAccountFixture(file.slice(0, -'.json'.length)) });
139
- const next = rewriteScalars(texts[idx], clean);
230
+ const clean = secure(scrubber.scrub(parsed[idx], { ownAccount: isOwnAccountFixture(file.slice(0, -'.json'.length)) }));
231
+ // The security pass can DROP fields (a red-team record keeps an allowlist), which
232
+ // a scalar splice cannot express; then the file is written whole.
233
+ const next = sameKeys(parsed[idx], clean) ? rewriteScalars(texts[idx], clean) : `${JSON.stringify(clean, null, 2)}\n`;
140
234
  if (next === texts[idx]) return;
141
235
  fs.writeFileSync(path.join(dir, file), next);
142
236
  changed += 1;