@bongos/core 1.20.47 → 1.20.49

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 (73) hide show
  1. package/.bongos-core.json +108 -73
  2. package/docs/copy-inventory.md +200 -184
  3. package/docs/copy-registry.json +364 -220
  4. package/docs/module-api-changelog.md +4 -0
  5. package/docs/page-inventory.json +11 -4
  6. package/docs/page-readings.json +411 -386
  7. package/modules/hall-ui/public/atlas.html +1 -1
  8. package/modules/hall-ui/public/blockers.html +1 -1
  9. package/modules/hall-ui/public/board-room.html +1 -1
  10. package/modules/hall-ui/public/collab.html +1 -1
  11. package/modules/hall-ui/public/commands.html +1 -1
  12. package/modules/hall-ui/public/copy-desk.html +1 -1
  13. package/modules/hall-ui/public/criterion-uat-ui.js +280 -0
  14. package/modules/hall-ui/public/deploy.html +1 -1
  15. package/modules/hall-ui/public/diagrams.html +1 -1
  16. package/modules/hall-ui/public/drachmae.html +1 -1
  17. package/modules/hall-ui/public/fleet.html +1 -1
  18. package/modules/hall-ui/public/gate.html +1 -1
  19. package/modules/hall-ui/public/goal-map.html +5 -3
  20. package/modules/hall-ui/public/goals-page.js +23 -44
  21. package/modules/hall-ui/public/goals-render.js +24 -15
  22. package/modules/hall-ui/public/goals.css +45 -0
  23. package/modules/hall-ui/public/goals.html +6 -3
  24. package/modules/hall-ui/public/goals.states.json +38 -0
  25. package/modules/hall-ui/public/government.html +1 -1
  26. package/modules/hall-ui/public/hall-render.js +3 -0
  27. package/modules/hall-ui/public/idea.html +1 -1
  28. package/modules/hall-ui/public/ideas.html +1 -1
  29. package/modules/hall-ui/public/index.html +1 -1
  30. package/modules/hall-ui/public/modules.html +1 -1
  31. package/modules/hall-ui/public/primer.html +1 -1
  32. package/modules/hall-ui/public/profile.html +1 -1
  33. package/modules/hall-ui/public/project-settings.html +1 -1
  34. package/modules/hall-ui/public/ranks.html +1 -1
  35. package/modules/hall-ui/public/roadmap.html +1 -1
  36. package/modules/hall-ui/public/roster.html +1 -1
  37. package/modules/hall-ui/public/sessions.html +1 -1
  38. package/modules/hall-ui/public/settings.html +1 -1
  39. package/modules/hall-ui/public/shell.js +82 -1
  40. package/modules/hall-ui/public/sky-data.js +9 -1
  41. package/modules/hall-ui/public/sky-interaction.js +1 -1
  42. package/modules/hall-ui/public/sky-panel.js +1 -1
  43. package/modules/hall-ui/public/studio.html +1 -1
  44. package/modules/hall-ui/public/style.css +36 -0
  45. package/modules/hall-ui/public/task.html +1 -1
  46. package/modules/hall-ui/public/thinking.html +6 -4
  47. package/modules/hall-ui/public/tweak-editor.html +1 -1
  48. package/modules/hall-ui/public/watch.html +1 -1
  49. package/modules/hall-ui/public/work-founding.states.json +39 -0
  50. package/modules/hall-ui/public/work.html +1 -1
  51. package/modules/hall-ui/records/founding-mode.md +31 -0
  52. package/modules/lifecycle/done-when.js +3 -1
  53. package/modules/lifecycle/sky.js +23 -8
  54. package/modules/onboarding/founding-mode.js +185 -0
  55. package/modules/onboarding/port.js +7 -0
  56. package/modules/ui-design/kit/fixtures/me-founding.json +24 -0
  57. package/modules/ui-design/kit/serve.js +3 -1
  58. package/package-lock.json +2 -2
  59. package/package.json +1 -1
  60. package/release-notes.json +12 -0
  61. package/scripts/hall-preview/server.js +2 -0
  62. package/src/bongos/routes/me.js +18 -1
  63. package/src/module-api.js +1 -1
  64. package/tests/claim_action.mjs +6 -1
  65. package/tests/criterion_uat_ui.mjs +113 -0
  66. package/tests/founding_mode.mjs +294 -0
  67. package/tests/goal_criterion_confirm_ui.mjs +188 -91
  68. package/tests/goal_map_page.mjs +2 -1
  69. package/tests/hall_sky.mjs +16 -0
  70. package/tests/hall_tweak_editor.mjs +1 -1
  71. package/tests/helpers.mjs +3 -1
  72. package/tests/nav_permission_atoms.mjs +1 -1
  73. package/tests/sky_read.mjs +7 -1
@@ -178,6 +178,51 @@
178
178
  white-space: nowrap; padding: 2px 8px; border-radius: var(--radius-pill);
179
179
  background: var(--warn-weak); color: var(--warn); font-size: 10.5px; font-weight: 600;
180
180
  }
181
+ /* task 1004402 (ADR 0351) — the UAT on a criterion. The state pill and the
182
+ Backend-only tag sit with the label; the panel spans the whole row under it.
183
+ Status tones reuse the hall's measured --ok / --warn pairs (DESIGN.md §Reuse):
184
+ awaiting reads as the warn state the "!" mark already uses, a UAT close as ok,
185
+ and the quieter closes (override, closed before UAT) in plain ink. */
186
+ .crit__badges { display: inline-flex; flex-wrap: wrap; gap: 4px; margin-left: 4px; vertical-align: 1px; }
187
+ .crit-state, .crit-tag {
188
+ white-space: nowrap; padding: 1px 7px; border-radius: var(--radius-pill);
189
+ font-size: 10.5px; font-weight: 600; line-height: 1.5;
190
+ background: var(--rule-soft); color: var(--ink-soft);
191
+ }
192
+ .crit-state--awaiting_uat { background: var(--warn-weak); color: var(--warn); }
193
+ .crit-state--satisfied_uat, .crit-state--satisfied_backend { background: var(--ok-weak); color: var(--ok); }
194
+ .crit-tag { background: transparent; border: var(--line) solid var(--rule); }
195
+ .crit__link {
196
+ cursor: pointer; white-space: nowrap; min-height: 24px; padding: 3px 6px; margin: 0 -6px;
197
+ border: 0; background: transparent; color: var(--ink-soft); font: inherit; font-size: 11.5px;
198
+ text-decoration: underline; text-underline-offset: 3px;
199
+ }
200
+ .crit__link:hover:not(:disabled) { color: var(--ink); }
201
+ .crit__link:disabled { opacity: .6; cursor: default; }
202
+ .uat-panel {
203
+ grid-column: 1 / -1; margin: 6px 0 4px 32px; padding: 12px 14px;
204
+ border-radius: var(--radius); background: color-mix(in srgb, var(--ink) 5%, var(--bg-card));
205
+ font-size: 12.5px; color: var(--ink-soft); display: flex; flex-direction: column; gap: 10px;
206
+ }
207
+ .uat-checks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
208
+ .uat-check { display: grid; grid-template-columns: 16px 64px minmax(0,1fr); gap: 6px; align-items: baseline; }
209
+ .uat-check__mark { color: var(--ink-faint); text-align: center; }
210
+ .uat-check--ok .uat-check__mark { color: var(--ok); font-weight: 700; }
211
+ .uat-check__label { font-weight: 600; color: var(--ink); }
212
+ .uat-video { width: 100%; max-width: 560px; border-radius: var(--radius); background: var(--ink); }
213
+ .uat-form { display: flex; flex-direction: column; gap: 8px; }
214
+ .uat-field { display: flex; flex-direction: column; gap: 4px; }
215
+ .uat-field input[type="file"] { min-height: 24px; font: inherit; font-size: 12px; }
216
+ .uat-field textarea {
217
+ font: inherit; font-size: 12.5px; padding: 6px 8px; border-radius: var(--radius);
218
+ border: var(--line) solid var(--rule); background: var(--bg-card); color: var(--ink); resize: vertical;
219
+ }
220
+ .uat-check-input { display: inline-flex; align-items: center; gap: 8px; min-height: 24px; cursor: pointer; }
221
+ .uat-check-input input { width: 16px; height: 16px; margin: 0; }
222
+ .uat-hint { margin: 0; }
223
+ .uat-error { margin: 0; color: var(--danger, var(--warn)); font-weight: 600; }
224
+ .uat-form .crit__confirm { align-self: flex-start; min-height: 32px; padding: 6px 14px; }
225
+
181
226
  /* BV1.R79: per-criterion expand in the capped band — short label, full text on
182
227
  click (matches the Roadmap page's .vbar__crit-d pattern). */
183
228
  .crit-d { min-width: 0; }
@@ -230,7 +230,7 @@
230
230
 
231
231
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
232
232
 
233
- <script src="/builders/shell.js?v=2026-09-30-commands"></script>
233
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
234
234
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
235
235
  <script src="/builders/palette.js" defer></script>
236
236
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -257,7 +257,10 @@
257
257
  began — goals-lib took the pure helpers, this takes the view painters.
258
258
  Loaded BEFORE goals-page.js, which calls window.OTBGoalsRender(ctx). -->
259
259
  <script src="/builders/bulk-toolbar.js"></script>
260
- <script src="/builders/goals-render.js?v=2026-09-30-bulktoolbar"></script>
261
- <script src="/builders/goals-page.js?v=2026-09-30-bulktoolbar"></script>
260
+ <!-- task 1004402: the criterion UAT pills, panel and sign-off (ADR 0351). Loaded
261
+ BEFORE goals-render.js, which reads window.OTBCriterionUat at factory time. -->
262
+ <script src="/builders/criterion-uat-ui.js?v=2026-09-30-uat2"></script>
263
+ <script src="/builders/goals-render.js?v=2026-09-30-uat2"></script>
264
+ <script src="/builders/goals-page.js?v=2026-09-30-uat2"></script>
262
265
  </body>
263
266
  </html>
@@ -128,6 +128,44 @@
128
128
  },
129
129
  "out": {
130
130
  "auth": false
131
+ },
132
+ "uat": {
133
+ "auth": true,
134
+ "url": "/builders/goals#/goal/1000111/completion",
135
+ "actions": [
136
+ [
137
+ "wait",
138
+ 800
139
+ ]
140
+ ],
141
+ "expect": {
142
+ "visible": [
143
+ "#goal-completion-full"
144
+ ]
145
+ }
146
+ },
147
+ "uat-panel": {
148
+ "auth": true,
149
+ "url": "/builders/goals#/goal/1000111/completion",
150
+ "actions": [
151
+ [
152
+ "wait",
153
+ 800
154
+ ],
155
+ [
156
+ "click",
157
+ "#goal-completion-full [data-uat-open=\"990001\"]"
158
+ ],
159
+ [
160
+ "wait",
161
+ 500
162
+ ]
163
+ ],
164
+ "expect": {
165
+ "visible": [
166
+ "#goal-completion-full .uat-form"
167
+ ]
168
+ }
131
169
  }
132
170
  },
133
171
  "reducedMotion": {
@@ -134,7 +134,7 @@
134
134
 
135
135
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
136
136
 
137
- <script src="/builders/shell.js?v=2026-09-30-commands"></script>
137
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
138
138
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
139
139
  <script src="/builders/palette.js" defer></script>
140
140
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -221,6 +221,9 @@
221
221
  // same reason: the index is where a builder lands, so omitting it here
222
222
  // would mean the badge appeared everywhere EXCEPT the page they open first.
223
223
  needs: (ctx.lastMeState && ctx.lastMeState.needs) || null,
224
+ // task 1004422: project founding mode — passed here for the same reason as the
225
+ // three above, or the index would be the one page the band never appears on.
226
+ founding: (ctx.lastMeState && ctx.lastMeState.founding_mode) || null,
224
227
  builder,
225
228
  });
226
229
 
@@ -52,7 +52,7 @@
52
52
 
53
53
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
54
54
 
55
- <script src="/builders/shell.js?v=2026-09-30-commands"></script>
55
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
56
56
  <script src="/builders/palette.js" defer></script>
57
57
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib" defer></script>
58
58
  <!-- task-detail.js still owns the #/idea/N and #/blocker/N overlay, so a ref
@@ -225,7 +225,7 @@
225
225
 
226
226
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
227
227
 
228
- <script src="/builders/shell.js?v=2026-09-30-commands"></script>
228
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
229
229
  <script src="/builders/palette.js" defer></script>
230
230
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
231
231
  <!-- task 1003069 (R08) — the quick-filing form's routed result links a task
@@ -106,7 +106,7 @@
106
106
  <script src="/builders/brand-apply.js?v=2026-06-27-r75" defer></script>
107
107
  <!-- The app shell (sidebar + top bar + view router). Dependency-free; loaded
108
108
  BEFORE builders.js so the chrome ids exist when events are wired. -->
109
- <script src="/builders/shell.js?v=2026-09-30-commands" defer></script>
109
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands" defer></script>
110
110
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
111
111
  <script src="/builders/palette.js" defer></script>
112
112
  <!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
@@ -101,7 +101,7 @@
101
101
 
102
102
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
103
103
 
104
- <script src="/builders/shell.js?v=2026-09-30-commands"></script>
104
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
105
105
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
106
106
  <script src="/builders/palette.js" defer></script>
107
107
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
@@ -115,7 +115,7 @@
115
115
 
116
116
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
117
117
 
118
- <script src="/builders/shell.js?v=2026-09-30-commands"></script>
118
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
119
119
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
120
120
  <script src="/builders/palette.js" defer></script>
121
121
  <!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
@@ -187,7 +187,7 @@
187
187
 
188
188
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
189
189
  chrome exists before the page script runs. -->
190
- <script src="/builders/shell.js?v=2026-09-30-commands"></script>
190
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
191
191
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
192
192
  <script src="/builders/palette.js" defer></script>
193
193
  <!-- shared frontend toolkit (window.OTB) — loaded before profile.js, which pulls from it. -->
@@ -58,7 +58,7 @@
58
58
 
59
59
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
60
60
 
61
- <script src="/builders/shell.js?v=2026-09-30-commands"></script>
61
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
62
62
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
63
63
  <script src="/builders/palette.js" defer></script>
64
64
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -68,7 +68,7 @@
68
68
 
69
69
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
70
70
  chrome exists before the page script runs. -->
71
- <script src="/builders/shell.js?v=2026-09-30-commands"></script>
71
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
72
72
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
73
73
  <script src="/builders/palette.js" defer></script>
74
74
  <!-- shared frontend toolkit (window.OTB) — loaded before ranks.js, which pulls from it. -->
@@ -41,7 +41,7 @@
41
41
 
42
42
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
43
43
 
44
- <script src="/builders/shell.js?v=2026-09-30-commands"></script>
44
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
45
45
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
46
46
  <script src="/builders/palette.js" defer></script>
47
47
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -50,7 +50,7 @@
50
50
 
51
51
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
52
52
  chrome exists before the page script runs. -->
53
- <script src="/builders/shell.js?v=2026-09-30-commands"></script>
53
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
54
54
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
55
55
  <script src="/builders/palette.js" defer></script>
56
56
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -99,7 +99,7 @@
99
99
 
100
100
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
101
101
  chrome exists before the page script runs. -->
102
- <script src="/builders/shell.js?v=2026-09-30-commands"></script>
102
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
103
103
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
104
104
  <script src="/builders/palette.js" defer></script>
105
105
  <!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
@@ -400,7 +400,7 @@
400
400
 
401
401
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
402
402
  chrome exists before the page scripts run. -->
403
- <script src="/builders/shell.js?v=2026-09-30-commands"></script>
403
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
404
404
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
405
405
  <script src="/builders/palette.js" defer></script>
406
406
  <!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
@@ -788,7 +788,7 @@
788
788
  // not establish authority. null is NOT "holds nothing that matters": it is
789
789
  // unreadable, and unreadable hides the link, the same call gateAllows()'s
790
790
  // `default: false` and the social mode's OFF-is-the-fallback rule make.
791
- var access = { authed: null, rank: null, graduated: false, showWork: false, permissions: null, needs: null };
791
+ var access = { authed: null, rank: null, graduated: false, showWork: false, permissions: null, needs: null, founding: null };
792
792
 
793
793
  function gateAllows(gate) {
794
794
  var authed = access.authed === true;
@@ -840,6 +840,79 @@
840
840
  else delete root.dataset.social;
841
841
  }
842
842
 
843
+ // PROJECT FOUNDING MODE (task 1004422 / BV2.PS03, goal 1000121). While a project is
844
+ // being founded, its EXISTING hall wears this mode — the owner's blend rule
845
+ // (docs/design/project-startup-direction.md): genesis never becomes a second hall.
846
+ // It adds exactly two things here and changes nothing else:
847
+ // - the founding band, a strip created ABOVE the topbar inside .app-body;
848
+ // - the asleep state on today's nav items, each saying which stage wakes it.
849
+ // The server decides which rooms sleep (/me.founding_mode.asleep, keyed by NAV id)
850
+ // and this never re-derives it — the social-mode precedent.
851
+ //
852
+ // OFF is the default and the fallback, and OFF REMOVES rather than hides: the band
853
+ // element and every asleep class and tag are taken out of the DOM, so a hall that
854
+ // is not founding is byte-for-byte the hall it was (tests/founding_mode.mjs pins
855
+ // it). Cosmetic only — an asleep room still opens; nothing is enforced (ADR 0016).
856
+ // The asleep tag in words a person would say, never a raw stage slug.
857
+ var WAKE_TEXT = {
858
+ scope: 'opens after scope', strategize: 'opens after strategy', charter: 'opens after the charter',
859
+ legalize: 'opens after legal setup', legislation: 'opens after legislation', alive: 'opens when alive'
860
+ };
861
+ function applyFounding() {
862
+ var f = access.founding;
863
+ var on = !!(f && f.active === true && f.asleep && typeof f.asleep === 'object');
864
+ var root = document.documentElement;
865
+ // The common case — a project that is not founding, on a page that never wore the
866
+ // mode — returns before touching a single node. That is the strongest form of the
867
+ // no-hijack condition: nothing is read, created, classed or removed.
868
+ if (!on && root.dataset.founding !== 'on') return;
869
+ document.querySelectorAll('#app-sidebar .nav-item--asleep').forEach(function (el) {
870
+ el.classList.remove('nav-item--asleep');
871
+ delete el.dataset.wakes;
872
+ var tag = el.querySelector('[data-wakes-tag]');
873
+ if (tag && tag.parentNode) tag.parentNode.removeChild(tag);
874
+ });
875
+ var band = document.getElementById('founding-band');
876
+ if (!on) {
877
+ delete root.dataset.founding;
878
+ if (band && band.parentNode) band.parentNode.removeChild(band);
879
+ return;
880
+ }
881
+ root.dataset.founding = 'on';
882
+ Object.keys(f.asleep).forEach(function (id) {
883
+ var el = document.getElementById('nav-' + id);
884
+ if (!el) return;
885
+ var stage = String(f.asleep[id]);
886
+ el.classList.add('nav-item--asleep');
887
+ el.dataset.wakes = stage;
888
+ var tag = document.createElement('span');
889
+ tag.className = 'nav-item__wakes';
890
+ tag.setAttribute('data-wakes-tag', '');
891
+ tag.textContent = WAKE_TEXT[stage] || 'opens later';
892
+ el.appendChild(tag);
893
+ });
894
+ if (!band) {
895
+ var body = document.querySelector('.app-body');
896
+ if (!body) return;
897
+ band = document.createElement('div');
898
+ band.id = 'founding-band';
899
+ band.className = 'founding-band';
900
+ band.setAttribute('role', 'status');
901
+ body.insertBefore(band, body.firstChild);
902
+ }
903
+ var name = (brand.identity && (brand.identity.productName || brand.identity.worldName)) || 'This project';
904
+ var stageText = f.stage === 'decide'
905
+ ? 'deciding the plan'
906
+ : 'stage ' + f.stage_number + ' of ' + f.stage_count + ' \u00b7 ' + f.stage;
907
+ band.innerHTML =
908
+ '<span class="founding-band__dot" aria-hidden="true"></span>' +
909
+ '<span class="founding-band__title">Project founding mode</span>' +
910
+ '<span class="founding-band__desc">' + esc(name) + ' is still being founded. Rooms open as each stage finishes, and everyone who joins now becomes a founding builder.</span>' +
911
+ '<span class="founding-band__stage">' + esc(stageText) + (f.day ? ' \u00b7 day ' + esc(String(f.day)) : '') +
912
+ (Array.isArray(f.waiting_on) && f.waiting_on.length ? ' \u00b7 waiting on ' + esc(f.waiting_on.join(', ')) : '') + '</span>' +
913
+ '<a class="founding-band__link" href="/">What\u2019s this?</a>';
914
+ }
915
+
843
916
  // task 1003117 (R06 of goal 1000074): EMPHASISE the Studio nav entry for a
844
917
  // builder whose craft includes artist — the "emphasized entry for artist-craft
845
918
  // builders" half of artist mode. Cosmetic only (the page is open to every rank,
@@ -917,6 +990,10 @@
917
990
  access.needs = (state.needs && typeof state.needs === 'object') ? state.needs : null;
918
991
  }
919
992
  if ('social' in state) applySocial(state.social);
993
+ // task 1004422: stored as given — the object, or null (OFF).
994
+ if ('founding' in state) {
995
+ access.founding = (state.founding && typeof state.founding === 'object') ? state.founding : null;
996
+ }
920
997
  if (state.builder) { renderUser(state.builder); applyCraftEmphasis(state.builder); }
921
998
  }
922
999
  document.querySelectorAll('#app-sidebar [data-gate]').forEach(function (el) {
@@ -930,6 +1007,8 @@
930
1007
  // what settles. Reversing the order would badge an item still hidden from
931
1008
  // the previous state.
932
1009
  applyCounts();
1010
+ // task 1004422: after the gate loop, so the asleep tags land on the settled items.
1011
+ applyFounding();
933
1012
  // AFTER the gate loop for the same reason applyCounts is: it reads the settled
934
1013
  // permission map, which the loop above is what the shell has just been told.
935
1014
  maybeRenderCoreUpdate();
@@ -1153,6 +1232,8 @@
1153
1232
  // hall's Standing card and the CLI nudge read — no new route, no extra
1154
1233
  // poll, and the badge cannot disagree with the roster it came from.
1155
1234
  needs: me.needs || null,
1235
+ // task 1004422: project founding mode, for the band and the asleep rooms.
1236
+ founding: me.founding_mode || null,
1156
1237
  builder: me.builder,
1157
1238
  });
1158
1239
  }).catch(function () { /* cosmetic only */ });
@@ -60,6 +60,14 @@
60
60
  const GOLDEN = 137.50776405003785 * Math.PI / 180;
61
61
  const reduce = !!(window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches);
62
62
  const NOW = Date.now();
63
+ // task 1004402 (ADR 0351): a ring's UAT state, in the SAME words the goal page
64
+ // uses — read from criterion-uat-ui.js, which both sky pages load first. An
65
+ // open ring carries no label (its open ring already says so), and a missing
66
+ // script or an older server yields none rather than a guessed word.
67
+ function uatLabel(state) {
68
+ const labels = window.OTBCriterionUat && window.OTBCriterionUat.STATE_LABELS;
69
+ return labels && state && state !== 'open' ? labels[state] || null : null;
70
+ }
63
71
  function hash(s) { let h = 2166136261; s = String(s); for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return (h >>> 0) / 4294967296; }
64
72
  function rng(seed) { let t = Math.floor(hash(seed) * 1e9) + 1; return () => { t += 0x6D2B79F5; let r = Math.imul(t ^ (t >>> 15), 1 | t); r ^= r + Math.imul(r ^ (r >>> 7), 61 | r); return ((r ^ (r >>> 14)) >>> 0) / 4294967296; }; }
65
73
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -138,7 +146,7 @@
138
146
  return {
139
147
  id: String(g.goal_id), title: g.title || 'A private goal', subtitle: g.subtitle || null, version: g.version_id, status: g.status,
140
148
  taskTotal: Number(g.task_total) || 0, taskDone: Number(g.task_done) || 0, createdAt: g.created_at || null,
141
- crit: (g.criteria || []).map((c) => ({ id: String(c.id), text: c.criterion_md || '', satisfied: c.satisfied === true, tasks: (c.task_ids || []).map(String), pendingReview: c.pending_review === true })),
149
+ crit: (g.criteria || []).map((c) => ({ id: String(c.id), text: c.criterion_md || '', satisfied: c.satisfied === true, tasks: (c.task_ids || []).map(String), pendingReview: c.pending_review === true, uat: c.uat_state || null, uatLabel: uatLabel(c.uat_state), backendOnly: c.backend_only === true })),
142
150
  tasks: (g.tasks || []).map((t) => ({ id: String(t.id), title: t.title || 'a task', status: t.status, credits: t.credits_reward == null ? 0 : Number(t.credits_reward), createdAt: t.created_on || null, sourceRef: t.idea_id != null ? String(t.idea_id) : null })),
143
151
  };
144
152
  }
@@ -31,7 +31,7 @@
31
31
  if (!h || h.kind === 'shuttle') { card.classList.remove('on'); card.setAttribute('aria-hidden', 'true'); return; }
32
32
  const { goalById } = S; let html = '';
33
33
  if (h.kind === 'idea') { const i = h.ref; const o = OBJ[i.st] || { noun: i.st, word: i.st }; const wellG = i.well && goalById.get(i.well); const r = reach(i).length; html = `<div class="k">${glyphHtml(i)}<span class="lbl${i.st === 'star' ? ' hot' : ''}">${o.noun} · ${o.word}${i.origin === 'claude' ? ' · filed by a session' : ''}</span></div><div class="t">${esc(i.title)}</div><div class="m">${i.shaped ? `<b>${i.shaped.kind === 'goal' ? 'became this goal' : 'became a ring'}</b> · ` : ''}${wellG ? `caught by <b>${esc(short(wellG.title))}</b>` : `open space · ${Math.round(i.age)} days adrift`} · ${esc(became(i))}${i.size && i.size.credits ? ` · size <b>${i.size.credits}</b>` : ''}${i.lit < 1 ? ` · <b>${Math.round(i.lit * 100)}%</b> alive` : ''}${r ? ' · its reach is drawn' : ''}</div>`; }
34
- else if (h.kind === 'ring') { const c = h.ref; const g = h.goal; html = `<div class="k"><span class="mk well" style="${c.satisfied ? 'border-style:solid;border-color:rgba(255,255,255,.7)' : ''}"></span><span class="lbl">ring ${h.index + 1} of ${g.crit.length} · ${c.satisfied ? 'closed' : 'open'}</span></div><div class="t">${esc(SKY.plain(c.text).slice(0, 180))}${SKY.plain(c.text).length > 180 ? '…' : ''}</div><div class="m">${c.tasks.length ? `<b>${c.tasks.length}</b> task${c.tasks.length === 1 ? '' : 's'} on this ring · <b>${c.credits}</b> credits` : 'no task gates it yet'}${c.pendingReview ? ' · awaiting review' : ''}${c.bornOf ? ` · <b style="color:var(--accent)">born of a thought</b>: ${esc(ideaTitle(c.bornOf))}` : ''}</div>`; }
34
+ else if (h.kind === 'ring') { const c = h.ref; const g = h.goal; html = `<div class="k"><span class="mk well" style="${c.satisfied ? 'border-style:solid;border-color:rgba(255,255,255,.7)' : ''}"></span><span class="lbl">ring ${h.index + 1} of ${g.crit.length} · ${c.satisfied ? 'closed' : 'open'}</span></div><div class="t">${esc(SKY.plain(c.text).slice(0, 180))}${SKY.plain(c.text).length > 180 ? '…' : ''}</div><div class="m">${c.tasks.length ? `<b>${c.tasks.length}</b> task${c.tasks.length === 1 ? '' : 's'} on this ring · <b>${c.credits}</b> credits` : 'no task gates it yet'}${c.uatLabel ? ` · ${esc(c.uatLabel)}` : (c.pendingReview && !c.uat ? ' · awaiting review' : '')}${c.backendOnly ? ' · backend-only' : ''}${c.bornOf ? ` · <b style="color:var(--accent)">born of a thought</b>: ${esc(ideaTitle(c.bornOf))}` : ''}</div>`; }
35
35
  else if (h.kind === 'goal') { const g = h.ref; if (!seen(g)) html = `<div class="k"><span class="mk unseen"></span><span class="lbl">unexplored</span></div><div class="t">Something is here.</div><div class="m">Follow a route from a goal you know to reach it — or turn explore off to see everything.</div>`; else { const met = g.crit.filter((c) => c.satisfied).length; const state = g.status === 'open' ? (g.live > 0 ? 'gravity well · alive' : 'gravity well') : g.status === 'achieved' ? 'settled system' : 'collapsed'; html = `<div class="k"><span class="mk ${g.status === 'open' ? (g.live > 0 ? 'alive' : 'well') : g.status === 'achieved' ? 'settled' : 'collapsed'}"></span><span class="lbl${g.live > 0 ? ' hot' : ''}">${state} · ${esc(g.band.label)}</span></div><div class="t">${esc(g.title)}</div><div class="m"><b>${g.taskDone}</b> of <b>${g.taskTotal}</b> tasks${g.credits ? ` · <b>${g.credits}</b> credits` : ''} · ${g.crit.length ? `<b>${met}</b> of <b>${g.crit.length}</b> rings closed` : 'no rings written'}${g.live > 0 ? ` · <b>${g.active}</b> held now, <b>${g.recent}</b> landed this week` : ''}${g.thoughts.filter(inLayer).length ? ` · <b>${g.thoughts.filter(inLayer).length}</b> of your thoughts` : ''}${g.bornOf ? ` · <b style="color:var(--accent)">born of a thought</b>` : ''}${edgesOf(g).length ? ` · <b>${edgesOf(g).length}</b> route${edgesOf(g).length === 1 ? '' : 's'}` : ''}</div>`; } }
36
36
  else if (h.kind === 'task' || h.kind === 'ltask') { const t = h.ref; const kin = t.goal && t.kin ? t.goal.tasks.filter((x) => x.kin === t.kin && x !== t).length : 0; html = `<div class="k"><span class="mk task${t.status === 'active' ? ' live' : ''}"></span><span class="lbl${t.status === 'active' ? ' hot' : ''}">task · ${esc(t.status)}</span></div><div class="t">${esc(t.title)}</div><div class="m">${t.credits != null ? `<b>${t.credits}</b> credits · ` : ''}${t.ci >= 0 ? `ring ${t.ci + 1}` : 'outer orbit'}${t.sourceRef ? ' · born of a thought' : ''}${kin ? ` · lines up with <b>${kin}</b> kin` : ''}${t.drift ? ' · in motion' : ''}</div>`; }
37
37
  else if (h.kind === 'stub') { const e = h.edge; const waits = e.a === h.goal; html = `<div class="k"><span class="mk route"></span><span class="lbl chart">route · ${waits ? 'waits on' : 'waited on by'}</span></div><div class="t">${seen(h.other) ? esc(h.other.title) : 'an unexplored goal'}</div><div class="m"><b>${e.count}</b> task dependenc${e.count === 1 ? 'y' : 'ies'} · click to chart it · double-click to chart every route from here</div>`; }
@@ -78,7 +78,7 @@
78
78
  ${waiting.length ? fact('waits on', waiting.map(name).join(', ')) : ''}
79
79
  ${waitedOn.length ? fact('waited on by', waitedOn.map(name).join(', ')) : ''}
80
80
  </div>
81
- ${g.crit.length ? `<ul class="crit">${g.crit.slice(0, 10).map((c) => `<li><span class="ring${c.satisfied ? ' on' : ''}"></span><span>${esc(plain(c.text).slice(0, 120))}${plain(c.text).length > 120 ? '…' : ''}${c.bornOf ? ` <span class="born">· born of a thought</span>` : ''}${c.credits ? ` <span class="lbl">${c.credits} cr</span>` : ''}</span></li>`).join('')}</ul>` : ''}
81
+ ${g.crit.length ? `<ul class="crit">${g.crit.slice(0, 10).map((c) => `<li><span class="ring${c.satisfied ? ' on' : ''}"></span><span>${esc(plain(c.text).slice(0, 120))}${plain(c.text).length > 120 ? '…' : ''}${c.bornOf ? ` <span class="born">· born of a thought</span>` : ''}${c.credits ? ` <span class="lbl">${c.credits} cr</span>` : ''}${c.uatLabel ? ` <span class="lbl">${esc(c.uatLabel)}</span>` : ''}${c.backendOnly ? ' <span class="lbl">backend-only</span>' : ''}</span></li>`).join('')}</ul>` : ''}
82
82
  <div class="doors">
83
83
  <button type="button" class="door primary" id="descendBtn">${g.status === 'open' ? 'descend into the well' : 'look closer'}</button>
84
84
  ${edgesOf(g).length ? `<button type="button" class="door" id="chartBtn">chart its routes</button>` : ''}
@@ -176,7 +176,7 @@
176
176
 
177
177
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
178
178
 
179
- <script src="/builders/shell.js?v=2026-09-30-commands"></script>
179
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
180
180
  <script src="/builders/palette.js" defer></script>
181
181
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
182
182
  <!-- The kit, for its ONE renderer: the full-size ship-time visual figure
@@ -667,6 +667,42 @@ main:focus { outline: none; }
667
667
  .nav-item--craft .nav-icon,
668
668
  .nav-item--craft .nav-item__label { color: var(--accent-ink); }
669
669
 
670
+ /* task 1004422 (BV2.PS03): PROJECT FOUNDING MODE. The hall wears it; it never
671
+ becomes a second hall (the blend rule, docs/design/project-startup-direction.md).
672
+ Two additions only, both created by shell.js while /me says founding and REMOVED
673
+ when it does not: the band above the topbar, and today's nav items dimmed with
674
+ the stage that wakes them. The band is live state, so its dot takes the accent;
675
+ the wake tag reads only in the open rail, like the labels. */
676
+ .founding-band {
677
+ display: flex;
678
+ align-items: center;
679
+ gap: 12px;
680
+ min-height: 44px;
681
+ padding: 0 22px;
682
+ background: color-mix(in srgb, var(--accent) 14%, var(--bg));
683
+ border-bottom: 1px solid color-mix(in srgb, var(--accent) 36%, transparent);
684
+ font-size: 13px;
685
+ color: var(--ink-soft);
686
+ }
687
+ .founding-band__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex: none; }
688
+ .founding-band__title { font-size: 11.5px; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink); white-space: nowrap; }
689
+ .founding-band__desc { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
690
+ .founding-band__stage { margin-left: auto; font-size: 12px; color: var(--accent-ink); white-space: nowrap; }
691
+ /* The prose-link 24px floor convention: width bought with padding, handed back with
692
+ an equal negative margin, so the target grows and the row does not re-space. */
693
+ .founding-band__link { padding: 10px 6px; margin: 0 -6px; color: var(--ink); text-decoration: underline; text-underline-offset: 3px; white-space: nowrap; }
694
+ /* Phone: the row wraps instead of widening the page (the no-sideways rule the kit
695
+ measured), and the long sentence steps aside for the stage and the link. */
696
+ @media (max-width: 640px) {
697
+ .founding-band { flex-wrap: wrap; row-gap: 2px; padding: 8px 16px; }
698
+ .founding-band__desc { display: none; }
699
+ .founding-band__stage { margin-left: 0; white-space: normal; }
700
+ }
701
+ .nav-item--asleep { opacity: 0.55; }
702
+ .nav-item__wakes { display: none; margin-left: auto; font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-faint); }
703
+ .app-sidebar:hover .nav-item__wakes,
704
+ .app-sidebar:focus-within .nav-item__wakes { display: inline; }
705
+
670
706
  /* task 1003737: the nav count badge — the hall's only badge mechanism, built
671
707
  once and generically (shell.js `count:` names a need id; see the NAV model).
672
708
  ABSOLUTE, not inline, because the rail has two shapes: it is a 64px icon strip
@@ -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-commands"></script>
60
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></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. -->
@@ -138,15 +138,17 @@
138
138
  </div>
139
139
  </div>
140
140
 
141
- <script src="/builders/shell.js?v=2026-09-30-commands"></script>
141
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></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>
145
- <script src="/builders/sky-data.js?v=2026-09-27-split"></script>
145
+ <!-- task 1004402: the UAT state words the rings share with the goal page. -->
146
+ <script src="/builders/criterion-uat-ui.js?v=2026-09-30-uat1"></script>
147
+ <script src="/builders/sky-data.js?v=2026-09-30-uat1"></script>
146
148
  <script src="/builders/sky-layout.js?v=2026-09-27-split"></script>
147
149
  <script src="/builders/sky-draw.js?v=2026-09-27-split"></script>
148
- <script src="/builders/sky-interaction.js?v=2026-09-27-split"></script>
149
- <script src="/builders/sky-panel.js?v=2026-09-27-split"></script>
150
+ <script src="/builders/sky-interaction.js?v=2026-09-30-uat1"></script>
151
+ <script src="/builders/sky-panel.js?v=2026-09-30-uat1"></script>
150
152
  <script src="/builders/sky-composer.js?v=2026-09-27-split"></script>
151
153
  <script src="/builders/sky-tour.js?v=2026-09-27-split"></script>
152
154
  </body>
@@ -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-commands"></script>
143
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></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-commands"></script>
165
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></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. -->
@@ -0,0 +1,39 @@
1
+ {
2
+ "_": "Project founding mode (task 1004422) on an ordinary hall page: the founding band above the topbar and today's nav items asleep, rendered from a /me that says founding (kit fixture me-founding.json, the real server shaper's output at stage strategize). The page itself is work.states.json's; only the identity differs. The no-founding case is every other hall states file.",
3
+ "page": "/builders/work",
4
+ "surface": "hall-ui",
5
+ "stub": {
6
+ "prefix": "/builders/",
7
+ "fixtures": {
8
+ "me": "me-founding"
9
+ }
10
+ },
11
+ "modeQuery": false,
12
+ "ignoreRequests": [
13
+ "/api/(gds|bongos)/me$",
14
+ "/api/(gds|bongos)/me/",
15
+ "/api/(gds|bongos)/tasks",
16
+ "/api/(gds|bongos)/claims",
17
+ "/api/(gds|bongos)/goals",
18
+ "/api/(gds|bongos)/inbox/",
19
+ "/api/(gds|bongos)/versions"
20
+ ],
21
+ "ignoreConsole": [
22
+ "/api/(gds|bongos)/",
23
+ "\\b401\\b",
24
+ "\\b404\\b",
25
+ "Unauthorized",
26
+ "Not Found",
27
+ "load failed"
28
+ ],
29
+ "states": {
30
+ "founding": {
31
+ "auth": true,
32
+ "expect": {
33
+ "visible": [
34
+ "#founding-band"
35
+ ]
36
+ }
37
+ }
38
+ }
39
+ }
@@ -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-commands"></script>
174
+ <script src="/builders/shell.js?v=2026-09-30-founding-commands"></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. -->