@bongos/core 1.21.112 → 1.21.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/.bongos-core.json +616 -186
  2. package/.claude/skills/ask-for-help/SKILL.md +1 -1
  3. package/.claude/skills/bongos-feedback/SKILL.md +1 -1
  4. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  5. package/.claude/skills/builder-ship/SKILL.md +1 -0
  6. package/GOVERNMENT.md +128 -11
  7. package/clients/bongos-client/README.md +1 -1
  8. package/clients/bongos-client/bongos-client.global.js +28 -0
  9. package/clients/bongos-client/index.cjs +28 -0
  10. package/clients/bongos-client/index.d.ts +45 -2
  11. package/clients/bongos-client/index.mjs +28 -0
  12. package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
  13. package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
  14. package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
  15. package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
  16. package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
  17. package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
  18. package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
  19. package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
  20. package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
  21. package/docs/adr/README.md +10 -1
  22. package/docs/api/openapi.json +989 -87
  23. package/docs/api-reference.md +18 -4
  24. package/docs/copy-inventory.md +343 -262
  25. package/docs/copy-registry.json +1223 -426
  26. package/docs/file-map.md +2 -1
  27. package/docs/module-api-changelog.md +4 -0
  28. package/docs/packs/engineer.md +2 -1
  29. package/docs/page-inventory.json +91 -6
  30. package/docs/page-readings.json +5017 -4458
  31. package/modules/builder-settings/law-locks.js +202 -0
  32. package/modules/builder-settings/routes/settings.js +66 -6
  33. package/modules/builder-settings/settings.js +7 -0
  34. package/modules/discord/ship-broadcast.js +22 -13
  35. package/modules/government/asks-db.js +54 -0
  36. package/modules/government/asks.js +269 -0
  37. package/modules/government/board-idea-reads.js +68 -0
  38. package/modules/government/board.js +21 -57
  39. package/modules/government/build-rights.js +148 -0
  40. package/modules/government/buildouts.js +160 -0
  41. package/modules/government/counts.js +81 -0
  42. package/modules/government/db.js +242 -0
  43. package/modules/government/decision-board.js +234 -8
  44. package/modules/government/decision-rules.js +2 -0
  45. package/modules/government/docket.js +114 -6
  46. package/modules/government/law-board.js +131 -0
  47. package/modules/government/laws.js +241 -0
  48. package/modules/government/leads.js +170 -0
  49. package/modules/government/migrations/government_027_laws.sql +143 -0
  50. package/modules/government/migrations/government_028_asks.sql +57 -0
  51. package/modules/government/migrations/government_029_leads.sql +135 -0
  52. package/modules/government/migrations/government_030_buildouts.sql +111 -0
  53. package/modules/government/migrations/government_031_treasury.sql +107 -0
  54. package/modules/government/module.json +2 -1
  55. package/modules/government/pollers/treasury-borrow.js +62 -0
  56. package/modules/government/positions.js +107 -0
  57. package/modules/government/presets.js +149 -0
  58. package/modules/government/routes/government.js +425 -2
  59. package/modules/government/routing.js +154 -0
  60. package/modules/government/treasury-db.js +166 -0
  61. package/modules/government/treasury.js +241 -0
  62. package/modules/hall-ui/public/atlas.html +2 -2
  63. package/modules/hall-ui/public/blockers.html +2 -2
  64. package/modules/hall-ui/public/board-room.html +2 -2
  65. package/modules/hall-ui/public/board-room.js +2 -0
  66. package/modules/hall-ui/public/city-draw.js +604 -11
  67. package/modules/hall-ui/public/city.css +157 -1
  68. package/modules/hall-ui/public/city.html +37 -7
  69. package/modules/hall-ui/public/city.js +441 -21
  70. package/modules/hall-ui/public/city.states.json +110 -5
  71. package/modules/hall-ui/public/collab.html +2 -2
  72. package/modules/hall-ui/public/commands.html +2 -2
  73. package/modules/hall-ui/public/copy-desk.html +2 -2
  74. package/modules/hall-ui/public/craft-mode.js +112 -29
  75. package/modules/hall-ui/public/deploy.html +2 -2
  76. package/modules/hall-ui/public/diagram-viewer.css +12 -0
  77. package/modules/hall-ui/public/diagram-viewer.js +4 -0
  78. package/modules/hall-ui/public/diagrams.html +2 -2
  79. package/modules/hall-ui/public/drachmae.html +2 -2
  80. package/modules/hall-ui/public/fleet.html +2 -2
  81. package/modules/hall-ui/public/gate.html +2 -2
  82. package/modules/hall-ui/public/goal-map.html +2 -2
  83. package/modules/hall-ui/public/goals.html +2 -2
  84. package/modules/hall-ui/public/gov-feeds.js +141 -0
  85. package/modules/hall-ui/public/gov-living.js +1002 -0
  86. package/modules/hall-ui/public/gov-positions.js +97 -0
  87. package/modules/hall-ui/public/government.html +2 -2
  88. package/modules/hall-ui/public/hall-render.js +3 -0
  89. package/modules/hall-ui/public/idea.html +2 -2
  90. package/modules/hall-ui/public/ideas.html +2 -2
  91. package/modules/hall-ui/public/index.html +3 -3
  92. package/modules/hall-ui/public/modules.html +2 -2
  93. package/modules/hall-ui/public/my-agents.css +334 -0
  94. package/modules/hall-ui/public/my-agents.html +72 -0
  95. package/modules/hall-ui/public/my-agents.js +603 -0
  96. package/modules/hall-ui/public/my-agents.states.json +37 -0
  97. package/modules/hall-ui/public/primer.html +2 -2
  98. package/modules/hall-ui/public/profile-connections.js +40 -28
  99. package/modules/hall-ui/public/profile.html +2 -2
  100. package/modules/hall-ui/public/project-settings.html +2 -2
  101. package/modules/hall-ui/public/ranks.html +2 -2
  102. package/modules/hall-ui/public/roadmap.html +2 -2
  103. package/modules/hall-ui/public/room.css +8 -0
  104. package/modules/hall-ui/public/roster.html +2 -2
  105. package/modules/hall-ui/public/sessions.html +2 -2
  106. package/modules/hall-ui/public/settings.html +2 -2
  107. package/modules/hall-ui/public/shell.js +116 -16
  108. package/modules/hall-ui/public/street-card.js +18 -1
  109. package/modules/hall-ui/public/street-deck.css +299 -0
  110. package/modules/hall-ui/public/street-deck.js +1145 -0
  111. package/modules/hall-ui/public/street-depth.js +712 -0
  112. package/modules/hall-ui/public/street-desk.js +5 -2
  113. package/modules/hall-ui/public/street-draw.js +109 -16
  114. package/modules/hall-ui/public/street-inside.js +137 -22
  115. package/modules/hall-ui/public/street-office.js +705 -0
  116. package/modules/hall-ui/public/street.css +161 -11
  117. package/modules/hall-ui/public/street.html +41 -11
  118. package/modules/hall-ui/public/street.js +78 -66
  119. package/modules/hall-ui/public/street.states.json +129 -21
  120. package/modules/hall-ui/public/studio.html +2 -2
  121. package/modules/hall-ui/public/style.css +5 -1
  122. package/modules/hall-ui/public/task.html +2 -2
  123. package/modules/hall-ui/public/thinking.html +2 -2
  124. package/modules/hall-ui/public/tweak-editor.html +2 -2
  125. package/modules/hall-ui/public/watch.html +2 -2
  126. package/modules/hall-ui/public/watch.js +14 -1
  127. package/modules/hall-ui/public/work.html +2 -2
  128. package/modules/hall-ui/records/living-city.md +57 -0
  129. package/modules/hall-ui/records/my-agents.md +21 -0
  130. package/modules/hall-ui/records/oversight-family.md +1 -0
  131. package/modules/hall-ui/records/profile-connections.md +10 -0
  132. package/modules/hall-ui/records/reading-rooms.md +2 -0
  133. package/modules/ideas/routes/blockers.js +92 -3
  134. package/modules/lifecycle/module.json +2 -1
  135. package/modules/lifecycle/moving.js +17 -1
  136. package/modules/lifecycle/routes/moving.js +6 -0
  137. package/modules/lifecycle/routes/project-settings.js +23 -0
  138. package/modules/onboarding/approval-broadcast.js +8 -0
  139. package/modules/onboarding/routes/access-requests.js +16 -6
  140. package/modules/platform-identity/connections.js +13 -2
  141. package/modules/platform-identity/routes/connections.js +7 -1
  142. package/modules/public-landing/public/account-private.states.json +2 -2
  143. package/modules/public-landing/public/account.html +65 -5
  144. package/modules/public-landing/public/account.states.json +3 -3
  145. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  146. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  147. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
  148. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  149. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  150. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
  151. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  152. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  153. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
  154. package/modules/public-landing/public/assets/world.css +4 -4
  155. package/modules/public-landing/public/connections-empty.states.json +14 -0
  156. package/modules/public-landing/public/connections-inactive.states.json +14 -0
  157. package/modules/public-landing/public/connections.html +450 -0
  158. package/modules/public-landing/public/connections.probes.json +414 -0
  159. package/modules/public-landing/public/connections.states.json +17 -0
  160. package/modules/public-landing/public/contact.html +2 -2
  161. package/modules/public-landing/public/guild.html +2 -2
  162. package/modules/public-landing/public/index.html +2 -2
  163. package/modules/public-landing/public/invitations.html +2 -2
  164. package/modules/public-landing/public/privacy.html +2 -2
  165. package/modules/public-landing/public/projects.html +2 -2
  166. package/modules/public-landing/public/projects.probes.json +6 -2
  167. package/modules/public-landing/public/terms.html +2 -2
  168. package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
  169. package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
  170. package/modules/ui-design/kit/fixtures/connections.json +22 -0
  171. package/modules/ui-design/kit/serve.js +95 -0
  172. package/package-lock.json +2 -2
  173. package/package.json +1 -1
  174. package/release-notes.json +128 -0
  175. package/scripts/discord/README.md +8 -5
  176. package/scripts/discord/post.js +12 -1
  177. package/scripts/gds/artifact-audit.js +20 -3
  178. package/scripts/gds/ask.js +164 -0
  179. package/scripts/gds/autobongos-run.js +20 -5
  180. package/scripts/gds/autobongos-tweak-lane.js +262 -0
  181. package/scripts/gds/autobongos-verify.js +21 -2
  182. package/scripts/gds/claim.js +2 -0
  183. package/scripts/gds/government-walk.js +638 -4
  184. package/scripts/gds/pulse.js +9 -6
  185. package/scripts/gds/ship-escalation.js +22 -3
  186. package/scripts/gds/ship-finish.js +5 -4
  187. package/scripts/gds/ship-merge-gate.js +18 -6
  188. package/scripts/gds/tweak-halt.js +1 -0
  189. package/scripts/hall-preview/README.md +41 -1
  190. package/scripts/hall-preview/pref-writes.js +140 -0
  191. package/scripts/hall-preview/refresh-fixtures.js +101 -7
  192. package/scripts/hall-preview/scrub-identity.js +366 -7
  193. package/scripts/hall-preview/server.js +27 -1
  194. package/scripts/render-diagrams.sh +4 -2
  195. package/src/bongos/auth-idp.js +6 -1
  196. package/src/bongos/project-settings.js +41 -0
  197. package/src/bongos/route-rank-check.js +27 -0
  198. package/src/bongos/routes/me.js +68 -7
  199. package/src/bongos/serve-internal.js +5 -1
  200. package/src/module-api.js +7 -1
  201. package/src/platform-server.js +10 -0
  202. package/tests/application_lifecycle.mjs +47 -5
  203. package/tests/autobongos_tweak_lane.mjs +208 -0
  204. package/tests/conductor_main_e2e.mjs +21 -0
  205. package/tests/connections_api.mjs +18 -0
  206. package/tests/craft_mode_nav.mjs +5 -3
  207. package/tests/craft_mode_tallies.mjs +52 -16
  208. package/tests/discord_approvals.mjs +11 -0
  209. package/tests/discord_channels.mjs +20 -0
  210. package/tests/fixtures/gov-seated.mjs +64 -0
  211. package/tests/fixtures/government-ask-cards.json +1284 -0
  212. package/tests/government_act_alone_buildings.mjs +1 -0
  213. package/tests/government_agent_asks.mjs +322 -0
  214. package/tests/government_asks.mjs +408 -0
  215. package/tests/government_build_rights.mjs +253 -0
  216. package/tests/government_buildouts.mjs +382 -0
  217. package/tests/government_counts.mjs +143 -0
  218. package/tests/government_decision_rules.mjs +5 -1
  219. package/tests/government_laws.mjs +488 -0
  220. package/tests/government_laws_session.mjs +321 -0
  221. package/tests/government_leads.mjs +375 -0
  222. package/tests/government_positions.mjs +127 -0
  223. package/tests/government_presets.mjs +215 -0
  224. package/tests/government_routes.mjs +8 -1
  225. package/tests/government_routing.mjs +118 -0
  226. package/tests/government_seed.mjs +5 -0
  227. package/tests/government_treasury.mjs +394 -0
  228. package/tests/governor_docket.mjs +26 -2
  229. package/tests/hall_buildouts.mjs +208 -0
  230. package/tests/hall_city.mjs +14 -6
  231. package/tests/hall_city_living.mjs +617 -0
  232. package/tests/hall_gov_counts.mjs +336 -0
  233. package/tests/hall_gov_living.mjs +636 -0
  234. package/tests/hall_gov_real_site.mjs +655 -0
  235. package/tests/hall_matter_card.mjs +6 -1
  236. package/tests/hall_my_agents.mjs +481 -0
  237. package/tests/hall_nav.mjs +107 -4
  238. package/tests/hall_page_gate_map.mjs +4 -0
  239. package/tests/hall_preview_governor_fixtures.mjs +30 -3
  240. package/tests/hall_preview_scrub.mjs +59 -1
  241. package/tests/hall_preview_security.mjs +169 -0
  242. package/tests/hall_reading_world.mjs +44 -0
  243. package/tests/hall_roster_world.mjs +6 -2
  244. package/tests/hall_street.mjs +18 -2
  245. package/tests/hall_street_amend.mjs +1 -1
  246. package/tests/hall_street_deck.mjs +609 -0
  247. package/tests/hall_street_depth.mjs +559 -0
  248. package/tests/hall_street_desk.mjs +1 -1
  249. package/tests/hall_street_inside.mjs +29 -5
  250. package/tests/hall_tweak_editor.mjs +4 -1
  251. package/tests/hub_account_page.mjs +94 -3
  252. package/tests/hub_connections_page.mjs +507 -0
  253. package/tests/hub_invite_badge.mjs +1 -1
  254. package/tests/idp_device_exchange.mjs +18 -0
  255. package/tests/landing_legal_pages.mjs +8 -5
  256. package/tests/landing_page.mjs +23 -4
  257. package/tests/nav_permission_atoms.mjs +4 -1
  258. package/tests/profile_connections_ui.mjs +88 -25
  259. package/tests/projects_hub.mjs +15 -0
  260. package/tests/render_check.mjs +2 -2
  261. package/tests/ship_broadcast_routing.mjs +57 -16
  262. package/tests/ship_broadcast_server.mjs +40 -7
  263. package/tests/ship_merge_gate.mjs +19 -0
  264. package/tests/ui_design_kit.mjs +2 -2
  265. package/tests/watch_applications_queue.mjs +23 -0
@@ -0,0 +1,202 @@
1
+ 'use strict';
2
+
3
+ // modules/builder-settings/law-locks.js — a project LAW's lock on a builder's
4
+ // personal setting, ENFORCED (task 1004707, goal 1000111 "government is how a
5
+ // project steers its AI").
6
+ //
7
+ // A law (modules/government/laws.js) may lock personal settings it overrides: a
8
+ // lock names a setting and either FIXES its value ({ setting, value }) or CAPS it
9
+ // ({ setting, max }). This file is where such a lock bites. It is enforced in two
10
+ // places, and the read is the one that cannot be dodged:
11
+ //
12
+ // 1. ON EVERY READ of the effective value. lockedPrefs() rewrites the builder's
13
+ // behavior_prefs so the effective profile obeys every lock, and describe()
14
+ // shapes the /me slices (interaction, wandering, model_allocation) from that.
15
+ // So a value stored BEFORE the law passed, or one written while the laws
16
+ // could not be read, still resolves to what the law says.
17
+ // 2. ON EVERY WRITE. checkPatch() refuses a PATCH that would set a locked
18
+ // setting to anything the law does not allow (409 locked_by_law), so the
19
+ // settings page cannot quietly store a choice that will never take effect.
20
+ // Clearing a setting back to its default (null) is always allowed: the read
21
+ // clamps the default too.
22
+ //
23
+ // WHICH SETTINGS. The personal settings a table really holds, spelled as the
24
+ // hall and laws.js spell them:
25
+ // technical_level, toolchain_familiarity, verbosity, machinery (interaction)
26
+ // wandering (rank-clamped)
27
+ // model.main, model.subagentDefault, model.subagentRoutine (model allocation)
28
+ // A lock naming any other setting (on_stuck, ask_format …) has nothing to bite on
29
+ // yet: it is reported `enforced: false, reason: 'not_a_personal_setting'`, and
30
+ // the law's own words still reach every session (the laws block).
31
+ //
32
+ // ORDER FOR A CAP. `max` reads along each setting's own ordered list: the
33
+ // interaction values in their declared order, wandering's four levels (or the
34
+ // level number), the model tiers cheap→capable. A value past the cap is pulled
35
+ // back to it; a value under it stands.
36
+ //
37
+ // The RANK still has the last word on wandering: a law can fix or cap it inside
38
+ // the rank's ceiling, never lift a builder past it (ADR 0016 — a law is a project
39
+ // rule, not a grant of authority).
40
+ //
41
+ // PURE: no db, no requires beyond the sibling pref libs. The routes and /me hand
42
+ // the laws in (the government port's lawsForSession), read per request.
43
+
44
+ const interactionPrefs = require('./interaction-prefs');
45
+ const wanderingPrefs = require('./wandering-prefs');
46
+ const modelAllocPrefs = require('./model-alloc-prefs');
47
+
48
+ const MODEL_PREFIX = 'model.';
49
+
50
+ // Each lockable setting: its ordered values, how to read its effective value off
51
+ // a prefs blob, and how to write one into a prefs blob (returning a new blob).
52
+ function settingSpec(setting) {
53
+ if (interactionPrefs.FIELD_KEYS.includes(setting)) {
54
+ return {
55
+ route: 'interaction',
56
+ order: Object.keys(interactionPrefs.FIELDS[setting].values),
57
+ read: (prefs) => interactionPrefs.effectiveProfile(prefs)[setting],
58
+ write: (prefs, v) => ({ ...(prefs || {}), [interactionPrefs.RESERVED_KEY]: { ...interactionPrefs.storedProfile(prefs), [setting]: v } }),
59
+ normalize: (v) => v,
60
+ };
61
+ }
62
+ if (setting === wanderingPrefs.RESERVED_KEY) {
63
+ return {
64
+ route: 'wandering',
65
+ order: wanderingPrefs.LEVELS.slice(),
66
+ read: (prefs, ctx) => wanderingPrefs.levelKey(wanderingPrefs.effectiveLevel(prefs, ctx.rank)),
67
+ write: (prefs, v) => ({ ...(prefs || {}), [wanderingPrefs.RESERVED_KEY]: wanderingPrefs.levelIndex(v) }),
68
+ // a level may be written as its key ('focused') or its number (1)
69
+ normalize: (v) => (wanderingPrefs.isLevelInt(v) ? wanderingPrefs.levelKey(v) : v),
70
+ };
71
+ }
72
+ if (setting.startsWith(MODEL_PREFIX)) {
73
+ const slot = setting.slice(MODEL_PREFIX.length);
74
+ if (!Object.prototype.hasOwnProperty.call(modelAllocPrefs.SLOTS, slot)) return null;
75
+ return {
76
+ route: 'model_allocation',
77
+ order: modelAllocPrefs.ALLOWED_TIERS.slice(),
78
+ read: (prefs, ctx) => modelAllocPrefs.effectiveAllocation(prefs, ctx.instanceModels)[slot],
79
+ write: (prefs, v) => ({ ...(prefs || {}), [modelAllocPrefs.RESERVED_KEY]: { ...modelAllocPrefs.storedOverride(prefs), [slot]: v } }),
80
+ normalize: (v) => v,
81
+ };
82
+ }
83
+ return null;
84
+ }
85
+
86
+ // Every lock the laws in force carry, flattened: { setting, value|max, law_id,
87
+ // law_no, law_text }. `laws`: the government port's lawsForSession().laws.
88
+ // laws.js refuses a setting locked by two laws at once, so the first wins here
89
+ // only as a belt.
90
+ function locksOf(laws) {
91
+ const out = [];
92
+ const seen = new Set();
93
+ (Array.isArray(laws) ? laws : []).forEach((law, i) => {
94
+ (law && Array.isArray(law.locks) ? law.locks : []).forEach((l) => {
95
+ if (!l || typeof l.setting !== 'string' || seen.has(l.setting)) return;
96
+ seen.add(l.setting);
97
+ const lock = { setting: l.setting, law_id: law.id == null ? null : String(law.id), law_no: law.no || i + 1, law_text: law.text || '' };
98
+ if (Object.prototype.hasOwnProperty.call(l, 'value')) lock.value = l.value;
99
+ else lock.max = l.max;
100
+ out.push(lock);
101
+ });
102
+ });
103
+ return out;
104
+ }
105
+
106
+ // What a lock allows for a value: the fixed value, or the cap applied.
107
+ // Returns { ok: true, value } or { ok: false, reason }.
108
+ function lockTarget(spec, lock, current) {
109
+ if (Object.prototype.hasOwnProperty.call(lock, 'value')) {
110
+ const v = spec.normalize(lock.value);
111
+ return spec.order.includes(v) ? { ok: true, value: v } : { ok: false, reason: 'value_not_allowed' };
112
+ }
113
+ const cap = spec.normalize(lock.max);
114
+ const j = spec.order.indexOf(cap);
115
+ if (j < 0) return { ok: false, reason: 'value_not_allowed' };
116
+ const i = spec.order.indexOf(current);
117
+ return { ok: true, value: i > j ? cap : current };
118
+ }
119
+
120
+ // Every value a lock lets a builder choose, in the setting's own order: the one
121
+ // fixed value, or every value up to and including the cap (task 1004714 — so a
122
+ // surface can offer the values under a cap instead of locking the whole row).
123
+ function allowedFor(spec, lock) {
124
+ if (Object.prototype.hasOwnProperty.call(lock, 'value')) {
125
+ const v = spec.normalize(lock.value);
126
+ return spec.order.includes(v) ? [v] : null;
127
+ }
128
+ const j = spec.order.indexOf(spec.normalize(lock.max));
129
+ return j < 0 ? null : spec.order.slice(0, j + 1);
130
+ }
131
+
132
+ // The builder's prefs with every lock applied, and what each lock did.
133
+ // ctx: { rank, instanceModels }
134
+ // Returns { prefs, locks: [{ setting, value|max, law_id, law_no, law_text,
135
+ // enforced, reason?, route?, from?, to?, allowed? }] } — `from` is what the
136
+ // builder's own setting resolves to without the laws, `to` what it resolves to
137
+ // under them, `allowed` every value the lock still lets them pick.
138
+ function lockedPrefs(behaviorPrefs, laws, ctx = {}) {
139
+ let prefs = behaviorPrefs && typeof behaviorPrefs === 'object' ? behaviorPrefs : {};
140
+ const report = [];
141
+ for (const lock of locksOf(laws)) {
142
+ const spec = settingSpec(lock.setting);
143
+ if (!spec) { report.push({ ...lock, enforced: false, reason: 'not_a_personal_setting' }); continue; }
144
+ const from = spec.read(behaviorPrefs || {}, ctx);
145
+ const target = lockTarget(spec, lock, spec.read(prefs, ctx));
146
+ if (!target.ok) { report.push({ ...lock, route: spec.route, enforced: false, reason: target.reason, from }); continue; }
147
+ prefs = spec.write(prefs, target.value);
148
+ report.push({ ...lock, route: spec.route, enforced: true, from, to: spec.read(prefs, ctx), allowed: allowedFor(spec, lock) });
149
+ }
150
+ return { prefs, locks: report };
151
+ }
152
+
153
+ // A PATCH, checked against the locks. `patch` maps setting → value (null clears,
154
+ // always allowed). Returns null when every key is allowed, else the first
155
+ // refusal: { setting, law_id, law_no, law_text, allowed }.
156
+ function checkPatch(patch, laws) {
157
+ const locks = locksOf(laws);
158
+ for (const [setting, raw] of Object.entries(patch || {})) {
159
+ if (raw === null || raw === undefined) continue;
160
+ const lock = locks.find((l) => l.setting === setting);
161
+ if (!lock) continue;
162
+ const spec = settingSpec(setting);
163
+ if (!spec) continue;
164
+ const v = spec.normalize(raw);
165
+ const target = lockTarget(spec, lock, v);
166
+ if (!target.ok) continue; // a lock naming no real value has nothing to enforce
167
+ if (target.value !== v) {
168
+ return { setting, law_id: lock.law_id, law_no: lock.law_no, law_text: lock.law_text, allowed: allowedFor(spec, lock) };
169
+ }
170
+ }
171
+ return null;
172
+ }
173
+
174
+ // The /me slices with the laws applied. The EFFECTIVE values and the injected
175
+ // contracts come from the locked prefs; what the builder has STORED is still
176
+ // reported as theirs, so a surface can say "yours: X, the law: Y".
177
+ // ctx: { rank, instanceModels }
178
+ function describe(behaviorPrefs, laws, ctx = {}) {
179
+ const raw = behaviorPrefs && typeof behaviorPrefs === 'object' ? behaviorPrefs : {};
180
+ const { prefs, locks } = lockedPrefs(raw, laws, ctx);
181
+ const interaction = interactionPrefs.describeForApi(prefs);
182
+ interaction.stored = interactionPrefs.storedProfile(raw);
183
+ const wandering = wanderingPrefs.describeForApi(prefs, ctx.rank);
184
+ wandering.override_level = wanderingPrefs.overrideLevel(raw);
185
+ const modelAllocation = modelAllocPrefs.describeForApi(prefs, ctx.instanceModels || {});
186
+ modelAllocation.override = modelAllocPrefs.storedOverride(raw);
187
+ return { interaction, wandering, model_allocation: modelAllocation, locks };
188
+ }
189
+
190
+ // The slice of a describe() a single settings route answers, with its locks.
191
+ function locksFor(route, locks) {
192
+ return (Array.isArray(locks) ? locks : []).filter((l) => l.route === route);
193
+ }
194
+
195
+ module.exports = {
196
+ settingSpec,
197
+ locksOf,
198
+ lockedPrefs,
199
+ checkPatch,
200
+ describe,
201
+ locksFor,
202
+ };
@@ -27,6 +27,7 @@ const eventSoundPrefs = require('../event-sound-prefs');
27
27
  const wanderingPrefs = require('../wandering-prefs');
28
28
  const interactionPrefs = require('../interaction-prefs');
29
29
  const modelAllocPrefs = require('../model-alloc-prefs');
30
+ const lawLocks = require('../law-locks');
30
31
  const renderPrefs = require('../render-prefs');
31
32
  const profileNudgePrefs = require('../profile-nudge-prefs');
32
33
  const displayName = require('../display-name');
@@ -214,6 +215,58 @@ module.exports = function buildBuilderSettingsRouter() {
214
215
  }
215
216
  });
216
217
 
218
+ // ---- the project's LAWS on these settings (task 1004707) ----
219
+ //
220
+ // A law passed by the project's government may LOCK a personal setting (fix it,
221
+ // or cap it). The lock is enforced here and in GET /me, never only shown:
222
+ // * every read below resolves the EFFECTIVE value with the locks applied
223
+ // (law-locks.js), and reports them as `law_locks` — so a value stored before
224
+ // the law passed still resolves to what the law says;
225
+ // * every write refuses a value a lock does not allow (409 locked_by_law).
226
+ // The laws are read per request through the government PORT (this module never
227
+ // requires another); with the government module off there are no laws.
228
+ async function lawsInForce() {
229
+ const gov = api.resolveOptional('government');
230
+ if (!gov || typeof gov.lawsForSession !== 'function') return [];
231
+ const out = await gov.lawsForSession();
232
+ return out && Array.isArray(out.laws) ? out.laws : [];
233
+ }
234
+ // A READ fails soft: the laws unreadable → the builder's own settings, said so
235
+ // (laws_read:false), rather than a broken settings page. A WRITE fails closed
236
+ // (lawsAllowingWrite): it is never let through on a guess.
237
+ async function lawsForRead() {
238
+ try { return { laws: await lawsInForce(), read: true }; } catch (err) {
239
+ log.warn('[gds] laws unreadable for a settings read', err);
240
+ return { laws: [], read: false };
241
+ }
242
+ }
243
+ // `known`: the laws this request already read (a write's check), so one
244
+ // request reads the book once.
245
+ async function lockedSlice(slice, stored, rank, known) {
246
+ const { laws, read } = known ? { laws: known, read: true } : await lawsForRead();
247
+ const d = lawLocks.describe(stored, laws, { rank, instanceModels: instanceModels() });
248
+ return { ...d[slice], law_locks: lawLocks.locksFor(slice, d.locks), laws_read: read };
249
+ }
250
+ // The write's check. Answers the refusal and returns null when a law forbids
251
+ // `settingPatch` (setting → value, the names laws use) or the laws cannot be
252
+ // read; otherwise returns the laws it read, for the response to reuse.
253
+ async function lawsAllowingWrite(res, settingPatch) {
254
+ let laws;
255
+ try { laws = await lawsInForce(); } catch (err) {
256
+ log.error('[gds] laws unreadable for a settings write', err);
257
+ res.fail('laws_unreadable', { status: 503, message: 'The project\'s laws could not be read just now, so nothing was saved. Try again.' });
258
+ return null;
259
+ }
260
+ const hit = lawLocks.checkPatch(settingPatch, laws);
261
+ if (!hit) return laws;
262
+ res.fail('locked_by_law', {
263
+ status: 409,
264
+ message: `Law ${hit.law_no} sets ${hit.setting} ("${hit.law_text}"); allowed: ${hit.allowed.join(', ')}`,
265
+ details: { setting: hit.setting, law_id: hit.law_id, law_no: hit.law_no, law: hit.law_text, allowed: hit.allowed },
266
+ });
267
+ return null;
268
+ }
269
+
217
270
  // Per-builder "wandering" level (task 1268, migration 119). Governs how
218
271
  // readily the agent opens a NEW THREAD of work (tangent / investigation /
219
272
  // unsolicited suggestion) vs. staying on the claimed task — off-task only,
@@ -227,7 +280,7 @@ module.exports = function buildBuilderSettingsRouter() {
227
280
  try {
228
281
  const builder = await api.getBuilderById(req.builder.id);
229
282
  const stored = await db.getBuilderBehaviorPrefs(req.builder.id);
230
- res.json(wanderingPrefs.describeForApi(stored, builder?.rank));
283
+ res.json(await lockedSlice('wandering', stored, builder?.rank));
231
284
  } catch (err) {
232
285
  log.error('[gds] GET /me/wandering', err);
233
286
  res.fail('read_failed', { status: 500, message: 'internal error' });
@@ -250,10 +303,12 @@ module.exports = function buildBuilderSettingsRouter() {
250
303
  if (!v.ok) {
251
304
  return res.fail('invalid_patch', 422, { errors: v.errors });
252
305
  }
306
+ const laws = await lawsAllowingWrite(res, v.patch);
307
+ if (!laws) return;
253
308
  const existing = await db.getBuilderBehaviorPrefs(req.builder.id);
254
309
  const next = wanderingPrefs.applyPatch(existing, v.patch);
255
310
  const saved = await db.setBuilderBehaviorPrefs(req.builder.id, next);
256
- res.json(wanderingPrefs.describeForApi(saved, builder?.rank));
311
+ res.json(await lockedSlice('wandering', saved, builder?.rank, laws));
257
312
  } catch (err) {
258
313
  log.error('[gds] PATCH /me/wandering', err);
259
314
  res.fail('write_failed', { status: 500, message: 'internal error' });
@@ -271,7 +326,7 @@ module.exports = function buildBuilderSettingsRouter() {
271
326
  router.get('/me/interaction', api.requireBuilder, async (req, res) => {
272
327
  try {
273
328
  const stored = await db.getBuilderBehaviorPrefs(req.builder.id);
274
- res.json(interactionPrefs.describeForApi(stored));
329
+ res.json(await lockedSlice('interaction', stored));
275
330
  } catch (err) {
276
331
  log.error('[gds] GET /me/interaction', err);
277
332
  res.fail('read_failed', { status: 500, message: 'internal error' });
@@ -287,11 +342,13 @@ module.exports = function buildBuilderSettingsRouter() {
287
342
  if (!v.ok) {
288
343
  return res.fail('invalid_patch', 422, { errors: v.errors });
289
344
  }
345
+ const laws = await lawsAllowingWrite(res, v.patch);
346
+ if (!laws) return;
290
347
  const existing = await db.getBuilderBehaviorPrefs(req.builder.id);
291
348
  const nextInteraction = interactionPrefs.applyPatch(interactionPrefs.storedProfile(existing), v.patch);
292
349
  const nextPrefs = { ...(existing || {}), [interactionPrefs.RESERVED_KEY]: nextInteraction };
293
350
  const saved = await db.setBuilderBehaviorPrefs(req.builder.id, nextPrefs);
294
- res.json(interactionPrefs.describeForApi(saved));
351
+ res.json(await lockedSlice('interaction', saved, undefined, laws));
295
352
  } catch (err) {
296
353
  log.error('[gds] PATCH /me/interaction', err);
297
354
  res.fail('write_failed', { status: 500, message: 'internal error' });
@@ -311,7 +368,7 @@ module.exports = function buildBuilderSettingsRouter() {
311
368
  router.get('/me/model-allocation', api.requireBuilder, async (req, res) => {
312
369
  try {
313
370
  const stored = await db.getBuilderBehaviorPrefs(req.builder.id);
314
- res.json(modelAllocPrefs.describeForApi(stored, instanceModels()));
371
+ res.json(await lockedSlice('model_allocation', stored));
315
372
  } catch (err) {
316
373
  log.error('[gds] GET /me/model-allocation', err);
317
374
  res.fail('read_failed', { status: 500, message: 'internal error' });
@@ -327,11 +384,14 @@ module.exports = function buildBuilderSettingsRouter() {
327
384
  if (!v.ok) {
328
385
  return res.fail('invalid_patch', 422, { errors: v.errors });
329
386
  }
387
+ // a slot is locked by its law name, model.<slot>
388
+ const laws = await lawsAllowingWrite(res, Object.fromEntries(Object.entries(v.patch).map(([k, t]) => [`model.${k}`, t])));
389
+ if (!laws) return;
330
390
  const existing = await db.getBuilderBehaviorPrefs(req.builder.id);
331
391
  const nextOverride = modelAllocPrefs.applyPatch(modelAllocPrefs.storedOverride(existing), v.patch);
332
392
  const nextPrefs = { ...(existing || {}), [modelAllocPrefs.RESERVED_KEY]: nextOverride };
333
393
  const saved = await db.setBuilderBehaviorPrefs(req.builder.id, nextPrefs);
334
- res.json(modelAllocPrefs.describeForApi(saved, instanceModels()));
394
+ res.json(await lockedSlice('model_allocation', saved, undefined, laws));
335
395
  } catch (err) {
336
396
  log.error('[gds] PATCH /me/model-allocation', err);
337
397
  res.fail('write_failed', { status: 500, message: 'internal error' });
@@ -32,6 +32,7 @@ const interactionPrefs = require('./interaction-prefs');
32
32
  const modelAllocPrefs = require('./model-alloc-prefs');
33
33
  const renderPrefs = require('./render-prefs');
34
34
  const builderNeeds = require('./builder-needs');
35
+ const lawLocks = require('./law-locks');
35
36
 
36
37
  module.exports = {
37
38
  // --- the raw behavior_prefs blob read (GET /me resolves this once, then shapes
@@ -45,6 +46,12 @@ module.exports = {
45
46
  describeModelAllocation: (...a) => modelAllocPrefs.describeForApi(...a),
46
47
  describeRender: (...a) => renderPrefs.describeForApi(...a),
47
48
  computeNeeds: (...a) => builderNeeds.computeNeeds(...a),
49
+ // task 1004707: the interaction / wandering / model-allocation slices with the
50
+ // project's laws' locks APPLIED (law-locks.js), plus what each lock did:
51
+ // (behaviorPrefs, laws, { rank, instanceModels }) → { interaction, wandering,
52
+ // model_allocation, locks }. GET /me uses this so a session reads the locked
53
+ // values, not the builder's own.
54
+ describeWithLaws: (...a) => lawLocks.describe(...a),
48
55
 
49
56
  // --- the per-builder model ceiling for a skill (cascade-dispatch clamps to it;
50
57
  // never exceeds it). Mirrors the former `skillPrefs.getForBuilder` call.
@@ -21,15 +21,24 @@
21
21
  // branch can NEVER return ship_news or ship_feed — a security fix must not
22
22
  // advertise the shape of an exploit on a public/builder channel before it is
23
23
  // live (the owner requirement behind task 990).
24
- // - otherwise → the builder #ship-feed only.
24
+ // - otherwise → the builder #ship-feed, every ship; and, for a
25
+ // NEWS_KINDS task only, the owner-facing #ship-news too.
25
26
  // #ship-feed / #security-ship-feed lines are PREFIXED with `#<taskId>` so the
26
27
  // upvote bot can map a 👍 back to the task (discord-reaction.parseShipMarker).
27
28
  //
28
- // #ship-news is PAUSED (task 1749, owner ask 2026-07-03): per-ship broadcast was
29
- // too spammy (it posted to #ship-news AND #ship-feed, ×2 posters = 4 messages).
30
- // For now #ship-news gets nothing so exactly one #ship-feed message fires per
31
- // ship. The broader "make the two channels genuinely different + filter by task
32
- // type" work is idea 458 — un-pause by restoring the ship_news entry below.
29
+ // THE TWO CHANNELS ARE EDITORIALLY DIFFERENT (task 1002846, idea 1000458; the
30
+ // owner's pick 2026-10-12 — a kind filter, not a daily digest). #ship-feed is the
31
+ // builders' ledger: every ship, `#<id>`-prefixed. #ship-news is the owner-facing
32
+ // headline stream (CLAUDE.md §2: short, no jargon): only what a person using the
33
+ // project would notice — a `feature` or a `bug` fix — as the plain value summary,
34
+ // no id. Cleanup, infra, refactor, spike and the rest stay on #ship-feed. A task
35
+ // whose kind is missing or unknown posts no headline: a quiet #ship-news is the
36
+ // safe failure. The headline is the value summary verbatim, author-written like
37
+ // the #ship-feed line; webhook.js posts every channel with allowed_mentions
38
+ // {parse:[]}, so a summary can never ping anyone from either. This replaces task 1749's pause, which muted #ship-news entirely
39
+ // because it then repeated every #ship-feed line (×2 posters = 4 messages a ship);
40
+ // the one poster is now the server's (ship-finish.js defers), so a news ship is
41
+ // two messages, in two channels that say different things.
33
42
  //
34
43
  // A PAGE TWEAK (task 1004439): an approved Tweak Mode round ships as a task, and
35
44
  // its line says what it is — a page approved, rewritten by its ARTIST (the round's
@@ -37,7 +46,8 @@
37
46
  // `pageArtist` is the artist's name already resolved, or null when it could not be
38
47
  // read; the line then says "page approved" and names nobody rather than the
39
48
  // applier. Still `#<id>`-prefixed, so applause on it reaches the round.
40
- function chooseBroadcastTargets({ securitySensitive, taskId, summary, pageTweak = false, pageArtist = null }) {
49
+ const NEWS_KINDS = new Set(['feature', 'bug']);
50
+ function chooseBroadcastTargets({ securitySensitive, taskId, summary, kind = null, pageTweak = false, pageArtist = null }) {
41
51
  if (!summary) return [];
42
52
  if (securitySensitive) {
43
53
  return [{ channel: 'security_ship_feed', line: `#${taskId} shipped (security) — ${summary}` }];
@@ -46,10 +56,9 @@ function chooseBroadcastTargets({ securitySensitive, taskId, summary, pageTweak
46
56
  const by = pageArtist ? ` — rewritten by ${plainName(pageArtist)}` : '';
47
57
  return [{ channel: 'ship_feed', line: `#${taskId} page approved${by}: ${summary}` }];
48
58
  }
49
- return [
50
- // { channel: 'ship_news', line: summary }, // PAUSED — task 1749 / idea 458
51
- { channel: 'ship_feed', line: `#${taskId} shipped — ${summary}` },
52
- ];
59
+ const targets = [{ channel: 'ship_feed', line: `#${taskId} shipped — ${summary}` }];
60
+ if (NEWS_KINDS.has(kind)) targets.push({ channel: 'ship_news', line: summary });
61
+ return targets;
53
62
  }
54
63
 
55
64
  // The page-tweak source string, and the artist's display name for a shipped
@@ -89,7 +98,7 @@ async function postShipBroadcast(task, deps = {}) {
89
98
  const log = deps.log || console.log;
90
99
  const pageTweak = task.source === PAGE_TWEAK_SOURCE;
91
100
  const pageArtist = pageTweak && sec === false ? await pageArtistName(task, deps) : null;
92
- const targets = chooseBroadcastTargets({ securitySensitive: sec, taskId: task.id, summary, pageTweak, pageArtist });
101
+ const targets = chooseBroadcastTargets({ securitySensitive: sec, taskId: task.id, summary, kind: task.kind, pageTweak, pageArtist });
93
102
  for (const { channel, line } of targets) {
94
103
  try {
95
104
  await postWebhook(channel, line);
@@ -155,4 +164,4 @@ async function postStrandNotice(info, deps = {}) {
155
164
  return result;
156
165
  }
157
166
 
158
- module.exports = { chooseBroadcastTargets, postShipBroadcast, chooseStrandTargets, postStrandNotice };
167
+ module.exports = { chooseBroadcastTargets, NEWS_KINDS, postShipBroadcast, chooseStrandTargets, postStrandNotice };
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ // modules/government/asks-db.js — where asks are stored and read (task 1004703).
4
+ //
5
+ // One row per docket matter in government_asks (government_028), keyed by the
6
+ // matter's docket id (`blockers:12`). saveAsk replaces a matter's ask whole (an
7
+ // agent rewriting its card after a refusal writes the new card, not a patch);
8
+ // asksFor reads the asks of many matters in one query, for the docket and the
9
+ // blockers list. The shape is checked in asks.js BEFORE anything reaches here,
10
+ // and the table's own CHECKs refuse a hand-written slop row too.
11
+ //
12
+ // The pool comes through the published doorway (ADR 0091 §2). No caching: an ask
13
+ // written a moment ago is in the next read.
14
+
15
+ const { pool } = require('../../src/module-api');
16
+ const asks = require('./asks');
17
+
18
+ const MAX_READ = 500;
19
+
20
+ // Store (or replace) one matter's ask. `ask` is checkAsk's stored form.
21
+ async function saveAsk({ matterId, ask, builderId }) {
22
+ if (!asks.isMatterId(matterId)) throw Object.assign(new Error('bad matter id'), { code: 'BAD_MATTER_ID' });
23
+ const { rows } = await pool.query(
24
+ `INSERT INTO government_asks
25
+ (matter_id, question, options, recommend_key, recommend_why, waits_on, if_nobody_answers, detail, written_by, written_at)
26
+ VALUES ($1, $2, $3::jsonb, $4, $5, $6, $7, $8, $9, now())
27
+ ON CONFLICT (matter_id) DO UPDATE
28
+ SET question = EXCLUDED.question, options = EXCLUDED.options,
29
+ recommend_key = EXCLUDED.recommend_key, recommend_why = EXCLUDED.recommend_why,
30
+ waits_on = EXCLUDED.waits_on, if_nobody_answers = EXCLUDED.if_nobody_answers,
31
+ detail = EXCLUDED.detail, written_by = EXCLUDED.written_by, written_at = now()
32
+ RETURNING matter_id, question, options, recommend_key, recommend_why, waits_on, if_nobody_answers, detail, written_at`,
33
+ [matterId, ask.question, JSON.stringify(ask.options), ask.recommend.key, ask.recommend.why,
34
+ ask.waits_on, ask.if_nobody_answers, ask.detail || null, builderId == null ? null : builderId],
35
+ );
36
+ return asks.fromRow(rows[0]);
37
+ }
38
+
39
+ // The asks of these matters → Map(matter_id → ask). Ids that are not matter ids
40
+ // are skipped; an empty list reads nothing.
41
+ async function asksFor(matterIds) {
42
+ const ids = [...new Set((Array.isArray(matterIds) ? matterIds : []).filter(asks.isMatterId))].slice(0, MAX_READ);
43
+ const out = new Map();
44
+ if (!ids.length) return out;
45
+ const { rows } = await pool.query(
46
+ `SELECT matter_id, question, options, recommend_key, recommend_why, waits_on, if_nobody_answers, detail, written_at
47
+ FROM government_asks WHERE matter_id = ANY($1::text[])`,
48
+ [ids],
49
+ );
50
+ for (const r of rows) out.set(r.matter_id, asks.fromRow(r));
51
+ return out;
52
+ }
53
+
54
+ module.exports = { saveAsk, asksFor, MAX_READ };