@bongos/core 1.20.49 → 1.20.51

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 (77) hide show
  1. package/.bongos-core.json +104 -79
  2. package/.claude/skills/new-project/SKILL.md +5 -0
  3. package/clients/bongos-client/index.d.ts +1 -1
  4. package/docs/api/openapi.json +3 -0
  5. package/docs/api-reference.md +1 -1
  6. package/docs/copy-inventory.md +367 -345
  7. package/docs/copy-registry.json +663 -463
  8. package/docs/module-api-changelog.md +4 -0
  9. package/docs/page-inventory.json +10 -1
  10. package/docs/page-readings.json +986 -947
  11. package/modules/hall-ui/public/atlas.html +1 -1
  12. package/modules/hall-ui/public/blockers.html +1 -1
  13. package/modules/hall-ui/public/board-room.html +1 -1
  14. package/modules/hall-ui/public/collab.html +1 -1
  15. package/modules/hall-ui/public/commands.html +1 -1
  16. package/modules/hall-ui/public/copy-desk.html +1 -1
  17. package/modules/hall-ui/public/deploy.html +1 -1
  18. package/modules/hall-ui/public/diagrams.html +1 -1
  19. package/modules/hall-ui/public/drachmae.html +1 -1
  20. package/modules/hall-ui/public/fleet.html +1 -1
  21. package/modules/hall-ui/public/gate.html +1 -1
  22. package/modules/hall-ui/public/goal-map.html +29 -14
  23. package/modules/hall-ui/public/goal-map.states.json +12 -0
  24. package/modules/hall-ui/public/goals.html +1 -1
  25. package/modules/hall-ui/public/government.html +1 -1
  26. package/modules/hall-ui/public/idea.html +1 -1
  27. package/modules/hall-ui/public/ideas.html +1 -1
  28. package/modules/hall-ui/public/index.html +1 -1
  29. package/modules/hall-ui/public/modules.html +1 -1
  30. package/modules/hall-ui/public/primer.html +1 -1
  31. package/modules/hall-ui/public/profile.html +1 -1
  32. package/modules/hall-ui/public/project-settings.html +1 -1
  33. package/modules/hall-ui/public/ranks.html +1 -1
  34. package/modules/hall-ui/public/roadmap.html +1 -1
  35. package/modules/hall-ui/public/roster.html +1 -1
  36. package/modules/hall-ui/public/sessions.html +1 -1
  37. package/modules/hall-ui/public/settings.html +1 -1
  38. package/modules/hall-ui/public/shell.js +145 -15
  39. package/modules/hall-ui/public/studio.html +1 -1
  40. package/modules/hall-ui/public/style.css +61 -5
  41. package/modules/hall-ui/public/task.html +1 -1
  42. package/modules/hall-ui/public/thinking.css +15 -11
  43. package/modules/hall-ui/public/thinking.html +1 -1
  44. package/modules/hall-ui/public/tweak-editor.css +6 -0
  45. package/modules/hall-ui/public/tweak-editor.html +2 -2
  46. package/modules/hall-ui/public/watch.html +1 -1
  47. package/modules/hall-ui/public/work.html +1 -1
  48. package/modules/hall-ui/records/hall-navigation.md +22 -0
  49. package/modules/hall-ui/records/sky.md +2 -0
  50. package/modules/platform-identity/routes/sso.js +5 -0
  51. package/modules/provisioning/migrations/provisioning_033_terms_ack.sql +33 -0
  52. package/modules/provisioning/onboard-plan.js +5 -0
  53. package/modules/provisioning/provisioning.js +7 -4
  54. package/modules/provisioning/routes/provisioning.js +3 -3
  55. package/modules/provisioning/terms-ack.js +36 -0
  56. package/modules/public-landing/public/projects.html +226 -77
  57. package/modules/public-landing/public/projects.probes.json +8 -2
  58. package/modules/public-landing/public/projects.states.json +16 -12
  59. package/modules/ui-design/kit/serve.js +6 -0
  60. package/package-lock.json +2 -2
  61. package/package.json +1 -1
  62. package/release-notes.json +12 -0
  63. package/src/module-api.js +1 -1
  64. package/tests/goal_map_page.mjs +10 -7
  65. package/tests/hall_audit.mjs +3 -4
  66. package/tests/hall_fullscreen.mjs +99 -14
  67. package/tests/hall_tweak_editor.mjs +1 -1
  68. package/tests/nav_permission_atoms.mjs +1 -1
  69. package/tests/projects_hub.mjs +4 -2
  70. package/tests/projects_hub_app_status.mjs +1 -1
  71. package/tests/projects_hub_app_step.mjs +9 -9
  72. package/tests/projects_hub_module_picker.mjs +7 -7
  73. package/tests/projects_hub_pre_uat.mjs +4 -1
  74. package/tests/provisioning_terms_ack.mjs +216 -0
  75. package/tests/wizard_draft_resume.mjs +4 -4
  76. package/tests/wizard_front_door.mjs +271 -0
  77. package/tests/wizard_intent_resume.mjs +31 -19
@@ -1051,15 +1051,71 @@ html[data-fullscreen="on"] .app-topbar {
1051
1051
  }
1052
1052
  html[data-fullscreen="on"] .app-topbar > :not(.topbar-side),
1053
1053
  html[data-fullscreen="on"] .topbar-side > :not(.fullscreen-toggle) { display: none; }
1054
+ /* task 1004430: the way back must read on ANY ground. The hall's glass is
1055
+ near-clear, so over the black sky in the light theme it was a dark glyph on
1056
+ black — the page looked stuck without its bars. The card tone plus a drop
1057
+ shadow and a ring keeps it legible on paper, on the sky and in either mode. */
1054
1058
  html[data-fullscreen="on"] .fullscreen-toggle {
1055
1059
  pointer-events: auto;
1056
- width: 36px; height: 36px;
1060
+ width: auto; height: 36px;
1061
+ gap: 6px;
1062
+ padding: 0 14px 0 10px;
1057
1063
  color: var(--ink);
1058
- background: var(--glass);
1059
- box-shadow: inset 0 0 0 1px var(--glass-edge);
1060
- backdrop-filter: blur(var(--glass-blur));
1061
- -webkit-backdrop-filter: blur(var(--glass-blur));
1064
+ background: var(--bg-card);
1065
+ box-shadow: 0 0 0 1px var(--rule), 0 4px 16px rgba(0, 0, 0, .35);
1066
+ font-size: 13px;
1067
+ font-weight: 500;
1068
+ white-space: nowrap;
1069
+ }
1070
+ /* A phone has no room for the words; the round button keeps its card tone. */
1071
+ @media (max-width: 640px) {
1072
+ html[data-fullscreen="on"] .fullscreen-toggle { width: 36px; padding: 0; }
1073
+ .fullscreen-toggle__label { display: none; }
1074
+ }
1075
+ html[data-fullscreen="on"] .fullscreen-toggle:hover { color: var(--accent-ink); }
1076
+ /* A page with no full-screen layout of its own steps its content clear of the
1077
+ corner button; the sky and the studio lay themselves out (shell.js `plain`). */
1078
+ html[data-fullscreen="on"][data-fullscreen-plain="on"] .app-content { padding-top: 60px; }
1079
+ /* Said once on entering, since the choice is remembered and a page can open in it. */
1080
+ .fullscreen-hint {
1081
+ position: fixed;
1082
+ top: 56px;
1083
+ right: 12px;
1084
+ z-index: var(--z-topbar);
1085
+ max-width: min(320px, calc(100vw - 24px));
1086
+ padding: 8px 12px;
1087
+ border-radius: 10px;
1088
+ background: var(--bg-card);
1089
+ color: var(--ink);
1090
+ font-size: 13px;
1091
+ line-height: 1.4;
1092
+ box-shadow: 0 0 0 1px var(--rule), 0 4px 16px rgba(0, 0, 0, .35);
1093
+ pointer-events: none;
1094
+ }
1095
+
1096
+ /* THE BACK LINK (task 1004430): at the head of the bar when the page was
1097
+ reached from inside another hall page. 24px floor on both axes. */
1098
+ .topbar-back {
1099
+ display: inline-flex;
1100
+ align-items: center;
1101
+ gap: 4px;
1102
+ min-height: 32px;
1103
+ min-width: 32px;
1104
+ padding: 0 10px 0 6px;
1105
+ margin-right: -4px;
1106
+ border-radius: var(--radius-pill);
1107
+ color: var(--ink-soft);
1108
+ font-size: 13px;
1109
+ text-decoration: none;
1110
+ white-space: nowrap;
1111
+ flex-shrink: 0;
1112
+ max-width: 40vw;
1113
+ overflow: hidden;
1114
+ text-overflow: ellipsis;
1062
1115
  }
1116
+ .topbar-back:hover { color: var(--ink); background: var(--rule-soft); }
1117
+ .topbar-back svg { width: 16px; height: 16px; flex-shrink: 0; }
1118
+ @media (max-width: 480px) { .topbar-back__label { display: none; } .topbar-back { padding: 0 8px; } }
1063
1119
  .zoom-toggle__pct {
1064
1120
  font-size: 12px;
1065
1121
  font-weight: 500;
@@ -57,7 +57,7 @@
57
57
 
58
58
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
59
59
  time so the chrome exists before the deferred page scripts execute. -->
60
- <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
60
+ <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
61
61
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
62
62
  <script src="/builders/palette.js" defer></script>
63
63
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
@@ -7,11 +7,11 @@
7
7
  ONE SHEET, TWO DOORS (part 4, task 1004232): /thinking and /goal-map are the
8
8
  same page opened in different lenses, so both are named below.
9
9
  IN THE HALL (task 1004327): /thinking now loads style.css and sits in the
10
- hall chrome; /goal-map is still standalone. So the sky's tokens and ground
11
- sit on /thinking's STAGE (main.sky-stage) instead of its body — the chrome
10
+ hall chrome, and /goal-map joined it in task 1004430. So the sky's tokens and
11
+ ground sit on the STAGE (main.sky-stage) instead of its body — the chrome
12
12
  around the stage keeps the hall's own palette in both modes — and the sky's
13
13
  few global rules are written :where()-scoped, so they cost no specificity
14
- (goal-map renders exactly as before) and never reach the chrome. */
14
+ and never reach the chrome. */
15
15
  /* The template declared these on :root. They sit on the page's body here,
16
16
  with the template's values untouched, because the server splices the
17
17
  branding pack's own :root (--ink, --accent, --rule …) into <head> AFTER this
@@ -21,7 +21,7 @@
21
21
  its own — tests/hall_reading_world.mjs; color-scheme inherits from body
22
22
  too). Same reason `body` below is qualified: a pack backdrop is written as
23
23
  `body{background:…}` and the void must win. */
24
- body[data-page="goal-map"], body[data-page="thinking"] .sky-stage {
24
+ body[data-page] .sky-stage {
25
25
  color-scheme: dark;
26
26
  --void: #000000; --ink: #ffffff; --ink-soft: #c0c0c0; --ink-faint: #a8a8a8; --ink-dim: #65656a; --ink-ghost: #3a3a3f;
27
27
  --rule: rgba(255,255,255,.16); --rule-soft: rgba(255,255,255,.08); --glass: rgba(255,255,255,.075); --glass-strong: rgba(255,255,255,.14);
@@ -30,15 +30,15 @@ body[data-page="goal-map"], body[data-page="thinking"] .sky-stage {
30
30
  --face: Manrope, "Segoe UI", system-ui, -apple-system, sans-serif;
31
31
  }
32
32
  html, body { height: 100%; }
33
- body[data-page="goal-map"], body[data-page="thinking"] .sky-stage { margin: 0; background: var(--void); color: var(--ink); font-family: var(--face); font-size: 15px; line-height: 1.5; overflow: hidden; overflow: clip; -webkit-font-smoothing: antialiased; }
33
+ body[data-page] .sky-stage { margin: 0; background: var(--void); color: var(--ink); font-family: var(--face); font-size: 15px; line-height: 1.5; overflow: hidden; overflow: clip; -webkit-font-smoothing: antialiased; }
34
34
  /* clip, not just hidden (task 1004342): a hidden-overflow box is still scrollable, so
35
35
  focus scrolled the stage sideways to show the footer hint and the whole sky slid
36
36
  left. clip cuts the same pixels and cannot be scrolled; hidden stays as the fallback. */
37
- body[data-page="thinking"] { overflow: hidden; overflow: clip; }
38
- :where(body[data-page="goal-map"], .sky-stage) button { font-family: inherit; }
39
- :where(body[data-page="goal-map"], .sky-stage) a { color: var(--accent); }
40
- :where(body[data-page="goal-map"], .sky-stage) .lbl { font: var(--label); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); }
41
- :where(body[data-page="goal-map"], .sky-stage) .mono { font: var(--mono); font-variant-numeric: tabular-nums; }
37
+ body[data-page="thinking"], body[data-page="goal-map"] { overflow: hidden; overflow: clip; }
38
+ :where(.sky-stage) button { font-family: inherit; }
39
+ :where(.sky-stage) a { color: var(--accent); }
40
+ :where(.sky-stage) .lbl { font: var(--label); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); }
41
+ :where(.sky-stage) .mono { font: var(--mono); font-variant-numeric: tabular-nums; }
42
42
  [hidden] { display: none !important; }
43
43
 
44
44
  /* THE STAGE (task 1004327). The box under the hall's top bar and beside its
@@ -55,11 +55,15 @@ html[data-fullscreen="on"] .sky-stage { top: 0; left: 0; }
55
55
  @media (max-width: 768px) { .sky-stage { left: 0; } }
56
56
  /* The full-screen switch floats top right over the stage, where the sky's
57
57
  tools end; the strip steps in by the button's width so nothing sits under it. */
58
- html[data-fullscreen="on"] .sky-stage .top { padding-right: 64px; }
58
+ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
59
59
  /* Under 900 the brand takes a row of its own (the template's rule below), so
60
60
  only that row steps in and the tools keep their width. */
61
61
  @media (max-width: 900px) {
62
62
  html[data-fullscreen="on"] .sky-stage .top { padding-right: 16px; }
63
+ html[data-fullscreen="on"] .sky-stage .brand { padding-right: 168px; }
64
+ }
65
+ /* Under 640 the exit is the round button again (style.css). */
66
+ @media (max-width: 640px) {
63
67
  html[data-fullscreen="on"] .sky-stage .brand { padding-right: 48px; }
64
68
  }
65
69
  /* style.css rules whose names the template also uses — each one reset to what
@@ -138,7 +138,7 @@
138
138
  </div>
139
139
  </div>
140
140
 
141
- <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
141
+ <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
142
142
  <script src="/builders/palette.js" defer></script>
143
143
 
144
144
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
@@ -136,10 +136,16 @@ html[data-fullscreen="on"] .tw-room { top: 0; left: 0; }
136
136
  /* In full screen the shell's one button floats over the night room in either
137
137
  theme, so on this page it wears the room's glass, not the hall's light ink. */
138
138
  html[data-fullscreen="on"] body[data-page="tweak-editor"] .fullscreen-toggle {
139
+ /* task 1004430: the shell's exit is a labelled pill, but this bar is laid out
140
+ for a 36px button in the corner, so the words would sit on submit. The
141
+ room keeps the round button; its name and title still say "Exit". */
142
+ width: 36px;
143
+ padding: 0;
139
144
  color: #ffffff;
140
145
  background: rgba(255, 255, 255, .07);
141
146
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .16);
142
147
  }
148
+ body[data-page="tweak-editor"] .fullscreen-toggle__label { display: none; }
143
149
 
144
150
  /* THE GLASS TOP BAR. Sticky, so the four doors stay in reach down a long page
145
151
  and the paper passes under the glass. In full screen it steps in far enough
@@ -49,7 +49,7 @@
49
49
  <!-- A <div>, not a <header>: the shell's top bar is the page's one
50
50
  <header> (the kit's one-header-main-h1 floor). -->
51
51
  <div class="tw-bar" id="tw-bar">
52
- <a class="tw-back" href="/builders/studio">← studio</a>
52
+ <a class="tw-back" data-own-back href="/builders/studio">← studio</a>
53
53
  <span class="tw-name"><span id="tw-name">Tweak a page</span> <span class="tw-surface" id="tw-surface"></span></span>
54
54
  <span class="tw-counter" id="tw-counter" aria-live="polite"></span>
55
55
  <div class="tw-actions">
@@ -140,7 +140,7 @@
140
140
 
141
141
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
142
142
 
143
- <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
143
+ <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
144
144
  <script src="/builders/palette.js" defer></script>
145
145
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
146
146
  <script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
@@ -162,7 +162,7 @@
162
162
 
163
163
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
164
164
  chrome exists before the page script runs. -->
165
- <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
165
+ <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
166
166
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
167
167
  <script src="/builders/palette.js" defer></script>
168
168
  <!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
@@ -171,7 +171,7 @@
171
171
 
172
172
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
173
173
  time so the chrome exists before the deferred page scripts execute. -->
174
- <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
174
+ <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
175
175
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
176
176
  <script src="/builders/palette.js" defer></script>
177
177
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
@@ -0,0 +1,22 @@
1
+ # Hall navigation — the Back link and the standard full-screen switch (task 1004430)
2
+
3
+ Covers: every shelled hall page (`shell.js`, `style.css`), plus `goal-map.html` joining the chrome.
4
+
5
+ ## The Back link
6
+
7
+ - `shell.js` renders `#topbar-back` ("← <label>") at the head of the top bar when the page was reached from INSIDE another hall page. The origin is `?from=` first, then a same-origin `document.referrer`; the label is that page's `NAV` label (or "task N" / "idea N" / "goal N" for a record).
8
+ - **A sidebar hop is not a drill-down.** A sidebar link click writes a one-shot `sessionStorage['hall-nav-hop']` naming its target; the page it lands on reads and deletes it, and shows no Back link. Without this every page reached from the sidebar would carry a Back link, which is just history.
9
+ - When the origin came from the referrer, the click runs `history.back()`, so a list comes back with its scroll and filters; the `href` is still the real address for a new tab.
10
+ - A page drawing its own record back link (`.rec-back` — `task.html`, `idea.html`) keeps it; the bar adds no second one. The origin's own `?from=` is dropped so back links never chain.
11
+ - `/profile?id=N` highlights **Builders**, not your own Profile, unless `/me` says the id is yours.
12
+
13
+ ## Full screen is standard
14
+
15
+ - Every shelled page gets the switch. `<body data-fullscreen-switch="off">` opts out. The bare attribute means "this page lays itself out for full screen" (sky, studio, tweak editor); a page without it is **plain**, and the shell sets `html[data-fullscreen-plain="on"]` so `style.css` pads `.app-content` clear of the corner button.
16
+ - In full screen the button is a labelled pill, an icon plus "Exit full screen", in `--bg-card` with a ring and a drop shadow, not the hall's near-clear glass. Under 640px it drops the words, and the tweak editor keeps the 36px round button because its bar is laid out for one. A page whose own back link is not `.rec-back` marks it `data-own-back`. On the black sky in the light theme the glass button was a dark icon on black, and because the choice is remembered per page, `/thinking` looked stuck with no bars. Entering full screen (or a page opening in it) also shows a 4.5s `#fullscreen-hint` saying how to leave.
17
+
18
+ ## The project sky joined the hall
19
+
20
+ `goal-map.html` now uses thinking.html's structure: sidebar and top-bar mounts, `style.css`, `shell.js`, the sky inside `<main id="hall-main" class="sky-stage">`, and a `<div class="brand">` because the shell's bar is the page's one `<header>`. In `thinking.css` both pages share one stage rule, `body[data-page] .sky-stage`. No hall page is standalone any more (`tests/hall_audit.mjs` `STANDALONE_EXEMPT` is empty).
21
+
22
+ Tests: `tests/hall_fullscreen.mjs` (the standard switch, `plain`, Back link cases), `tests/goal_map_page.mjs`, `tests/hall_audit.mjs` §8.
@@ -31,6 +31,8 @@ Pinned by `tests/hall_sky.mjs` (31 cases: the reads, the five objects, the two c
31
31
  - **The sky's palette lives on the stage, not the body, on `/thinking`** — so the chrome around it keeps the hall's own light and dark. `style.css` now loads under `thinking.css`, and the handful of hall rules whose names the template also uses (`.skip`, `.filters`, `.foot`, `:focus-visible`, text inputs, `* { box-sizing }`) are reset under `:where(.sky-stage)` at the same specificity, before the sky's own rules. The sky's global rules are `:where()`-scoped for the same reason, which leaves `/goal-map` byte-for-byte in its old cascade.
32
32
  - **Both pages render in both modes now.** The chrome follows the hall theme; the sky's stage is the void in both. The `full` and `full-back` states in both states files (and `full-list` in the sky's) press the switch.
33
33
 
34
+ **Superseded for `/goal-map` by task 1004430 (owner, 2026-09-30):** the project sky joined the hall chrome the same way, with the same switch. Both pages now share one stage rule (`body[data-page] .sky-stage`), `STANDALONE_EXEMPT` is empty, and the full-screen button uses the card tone so it can be seen on the void. See [`hall-navigation.md`](hall-navigation.md).
35
+
34
36
  ## Two skies, one renderer (task 1004328, BV2.TW21)
35
37
 
36
38
  **Owner decision 2026-09-27: "Your thinking is only your thinking."** Part 4's "one page, two doors" above is SUPERSEDED: the two pages share the renderer and nothing else. The owner confirmed that "the your projects tab" means the whole-project lens, its switch and the project-map features. What this settled:
@@ -25,6 +25,7 @@ const hubKeys = require('../hub-keys');
25
25
  const hubDevice = require('../hub-device');
26
26
  // The D7 applicant-profile read (task 1002972) and the echo that gates it.
27
27
  const applicantProfile = require('../applicant-profile');
28
+ const terms = require('../terms-acceptance');
28
29
  const applicationEcho = require('../application-echo');
29
30
  const instanceFeedback = require('../instance-feedback');
30
31
 
@@ -204,6 +205,10 @@ function registerSeams() {
204
205
  // Floors on the EXISTENCE predicate, not the rollup one: hide_stats
205
206
  // suppresses numbers, never addressability.
206
207
  getPublicHandle: (githubId) => canonicalProfile.getPublicHandle(githubId, { pool }),
208
+ // task 1004417 (BV2.PS05): the terms version /terms states, read by provisioning
209
+ // when a founder ticks the create wizard's terms box. A READ of the constant only —
210
+ // it stamps nothing on any account (that stays terms-acceptance.js's alone).
211
+ termsVersion: () => terms.TERMS_VERSION,
207
212
  });
208
213
  }
209
214
  }
@@ -0,0 +1,33 @@
1
+ -- provisioning_033_terms_ack.sql — the founder's terms acknowledgement, kept on the project
2
+ -- (task 1004417, BV2.PS05; goal 1000121, spec docs/specs/bongos-v2-project-startup.md).
3
+ --
4
+ -- WHY THIS EXISTS. The create wizard now opens on a terms screen: the founder ticks "I can
5
+ -- sign legally, or can get a signature, for the use of this platform, and I acknowledge the
6
+ -- terms and conditions" before anything else is asked. The owner chose (2026-09-30) to keep
7
+ -- that record ON THE PROJECT, not on the person's platform account: the account-level stamp
8
+ -- is ADR 0213's, written only at a witnessed sign-in (and task 1003361 adds the hub's own
9
+ -- sign-in as its second capture point). This one says who founded WHICH project under WHICH
10
+ -- text, which a later terms revision needs to know per project.
11
+ --
12
+ -- WHAT IT ADDS.
13
+ -- provisioning_instances.terms_ack_at — when the create that ticked the box was filed.
14
+ -- provisioning_instances.terms_ack_version — the terms version the SERVER stamped: read from
15
+ -- platform-identity's TERMS_VERSION through the platform-identity.hub port, never from the
16
+ -- request body, so a caller cannot record agreement to a text it names itself. NULL on an
17
+ -- instance with no hub to ask (the acknowledgement time is still kept).
18
+ --
19
+ -- The names are deliberately NOT terms_accepted_at / terms_version: those are the account's
20
+ -- acceptance columns, and tests/terms_acceptance.mjs fails any second server file that writes
21
+ -- them. This is a different record with a different owner.
22
+ --
23
+ -- Nullable, no backfill: a project created before this shipped never saw the screen, and a
24
+ -- NULL says exactly that. Written once, at create; an idempotent re-request never rewrites it.
25
+ --
26
+ -- Additive and namespaced (ADR 0083): no down-migration, idempotent — safe to re-run.
27
+
28
+ BEGIN;
29
+
30
+ ALTER TABLE provisioning_instances ADD COLUMN IF NOT EXISTS terms_ack_at timestamptz;
31
+ ALTER TABLE provisioning_instances ADD COLUMN IF NOT EXISTS terms_ack_version text;
32
+
33
+ COMMIT;
@@ -26,6 +26,11 @@
26
26
  // an existing repo; ADR 0121 §Decision 1 / task 2046). Every other leg is identical, so
27
27
  // `mode` only relabels the github-repo / init / scaffold steps.
28
28
  //
29
+ // The hub wizard's FRONT DOOR (task 1004417 — the terms tick, then demo or create) is
30
+ // deliberately NOT a step here: it comes before the rail, sets up nothing, and its record
31
+ // is the project's own terms columns (./terms-ack.js), not a plan key. The /new-project
32
+ // skill's "step 0" says the same for the other forms.
33
+ //
29
34
  // `opts.domain` (task 1002703): the terminal enter-hall step names the instance's own
30
35
  // hall address when the renderer knows the domain, and falls back to the `bongos dev`
31
36
  // local-hall one-liner when it doesn't — the hall is the point of creating a project,
@@ -1076,7 +1076,7 @@ async function listAllInstances(db, { status } = {}) {
1076
1076
  // re-fetches the existing row and returns { created:false } so the ROUTE can
1077
1077
  // decide (same owner → idempotent re-request; different owner → 409). Never
1078
1078
  // silently reassigns ownership.
1079
- async function createInstance(db, { ownerBuilderId, slug, targetRef, hostingShape, domain, dbName, port, tier, onboardMode, type, description, detail, modules }) {
1079
+ async function createInstance(db, { ownerBuilderId, slug, targetRef, hostingShape, domain, dbName, port, tier, onboardMode, type, description, detail, modules, termsAck }) {
1080
1080
  // Unknown/absent coerces to the shape that costs nothing and trusts nothing. Was
1081
1081
  // 'co-tenant' (then the free front door); that shape is now retired + unrequestable,
1082
1082
  // so coercing to it would mint a row no caller is allowed to ask for.
@@ -1103,12 +1103,15 @@ async function createInstance(db, { ownerBuilderId, slug, targetRef, hostingShap
1103
1103
  try {
1104
1104
  const { rows } = await db.query(
1105
1105
  `INSERT INTO provisioning_instances
1106
- (owner_builder_id, slug, target_ref, hosting_shape, domain, db_name, port, tier, cost_estimate_usd, onboard_mode, type, description, detail, modules)
1107
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb, $14::jsonb)
1106
+ (owner_builder_id, slug, target_ref, hosting_shape, domain, db_name, port, tier, cost_estimate_usd, onboard_mode, type, description, detail, modules, terms_ack_at, terms_ack_version)
1107
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb, $14::jsonb, CASE WHEN $15::boolean THEN now() END, $16)
1108
1108
  RETURNING *`,
1109
1109
  [ownerBuilderId, slug, targetRef ?? null, shape, domain ?? null,
1110
1110
  dbName ?? null, port ?? null, tier ?? null, cost, mode, declaredType,
1111
- told, JSON.stringify(answers), picked === null ? null : JSON.stringify(picked)]
1111
+ told, JSON.stringify(answers), picked === null ? null : JSON.stringify(picked),
1112
+ // the wizard's terms screen (task 1004417, ./terms-ack.js): the route resolved the
1113
+ // version server-side; no acknowledgement leaves both columns NULL
1114
+ !!termsAck, (termsAck && termsAck.version) || null]
1112
1115
  );
1113
1116
  return { instance: rows[0], created: true };
1114
1117
  } catch (e) {
@@ -56,7 +56,7 @@ const { recommendationsForDetails } = require('../recommendations');
56
56
  const { makeAskRateLimiter, clientIp } = require('../rate-limit');
57
57
  const { requireAuthorityForShape } = require('../paid-shape-gate');
58
58
  const { requireRoomOnBox } = require('../capacity-gate'); // a full box refused up front (task 1003938)
59
- const { adoptCredentialFault } = require('../credential-preflight');
59
+ const { adoptCredentialFault } = require('../credential-preflight'); const { termsAcknowledgement } = require('../terms-ack');
60
60
  const catalogBridge = require('../catalog-bridge');
61
61
  const capacity = require('../capacity'); // the box's slot ceiling (ADR 0285, task 1003927)
62
62
  const { callbackPage } = require('./callback-page');
@@ -323,7 +323,7 @@ module.exports = function provisioningRoutes() {
323
323
  // exactly the set the panel preselected. An explicit [] is the different
324
324
  // answer "none of them". maxItems is a sanity ceiling well above a catalog
325
325
  // that is three modules wide today.
326
- modules: { type: 'array', itemsType: 'string', maxItems: 50 },
326
+ modules: { type: 'array', itemsType: 'string', maxItems: 50 }, terms_acknowledged: { type: 'boolean' }, // the latter: the wizard's terms tick (task 1004417, ../terms-ack.js)
327
327
  })) return;
328
328
  const body = req.body || {};
329
329
  const detailFault = badProjectDetail(body.detail);
@@ -426,7 +426,7 @@ module.exports = function provisioningRoutes() {
426
426
  description: body.description, detail: body.detail,
427
427
  // The picker's selection (task 1002339) — absent for the same reason and
428
428
  // with the same standing: skipped, and resolved from the bundle on read.
429
- modules: body.modules,
429
+ modules: body.modules, termsAck: termsAcknowledgement(body),
430
430
  });
431
431
  if (req.releaseFreePlace) await req.releaseFreePlace(); // the row exists: the one-free lock's work is done (../free-place-lock.js)
432
432
  // Slug already taken by ANOTHER builder — refuse (never reassign ownership).
@@ -0,0 +1,36 @@
1
+ // modules/provisioning/terms-ack.js — the founder's terms acknowledgement, as the create
2
+ // route records it (task 1004417, BV2.PS05; migration provisioning_033).
3
+ //
4
+ // The wizard's first screen asks the founder to tick one box before anything else. The
5
+ // request carries only that the box was ticked (`terms_acknowledged: true`). WHICH terms
6
+ // is the server's answer, never the caller's: the version is read from platform-identity's
7
+ // TERMS_VERSION — the same constant /terms states and ADR 0213 stamps on an account —
8
+ // through the platform-identity.hub port. A body cannot name the text it agreed to, so a
9
+ // forged version is not a thing this record can hold.
10
+ //
11
+ // resolveOptional, not a `consumes` entry: platform-identity is hub-only (`default:
12
+ // false`), so an instance running provisioning without it has no terms version to ask for.
13
+ // The acknowledgement is still kept there, with a NULL version, rather than dropped —
14
+ // the founder did tick the box.
15
+ //
16
+ // Its own file because routes/provisioning.js sits a few lines under the 1500-line
17
+ // fitness ratchet.
18
+ 'use strict';
19
+
20
+ const api = require('../../src/module-api');
21
+
22
+ function currentTermsVersion() {
23
+ const hub = api.resolveOptional('platform-identity.hub');
24
+ const v = hub && typeof hub.termsVersion === 'function' ? hub.termsVersion() : null;
25
+ return typeof v === 'string' && v ? v : null;
26
+ }
27
+
28
+ // The record createInstance writes, or null when the box was not ticked (an older client,
29
+ // the CLI, an adopt run from a script — none of them showed the screen, and a NULL row
30
+ // says exactly that). Strictly `true`: the validator already refuses a non-boolean.
31
+ function termsAcknowledgement(body) {
32
+ if (!body || body.terms_acknowledged !== true) return null;
33
+ return { version: currentTermsVersion() };
34
+ }
35
+
36
+ module.exports = { termsAcknowledgement };