@bongos/core 1.21.44 → 1.21.46

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 (63) hide show
  1. package/.bongos-core.json +85 -65
  2. package/clients/bongos-client/index.d.ts +1 -1
  3. package/docs/api/openapi.json +2 -0
  4. package/docs/copy-inventory.md +23 -11
  5. package/docs/copy-registry.json +131 -13
  6. package/docs/module-api-changelog.md +4 -0
  7. package/docs/page-readings.json +755 -751
  8. package/modules/hall-ui/public/atlas.html +3 -3
  9. package/modules/hall-ui/public/blockers.html +3 -3
  10. package/modules/hall-ui/public/board-room.html +3 -3
  11. package/modules/hall-ui/public/city.html +3 -3
  12. package/modules/hall-ui/public/collab.html +3 -3
  13. package/modules/hall-ui/public/commands.html +3 -3
  14. package/modules/hall-ui/public/copy-desk.html +3 -3
  15. package/modules/hall-ui/public/craft-mode.js +8 -1
  16. package/modules/hall-ui/public/deploy.html +3 -3
  17. package/modules/hall-ui/public/diagrams.html +3 -3
  18. package/modules/hall-ui/public/drachmae.html +3 -3
  19. package/modules/hall-ui/public/fleet.html +3 -3
  20. package/modules/hall-ui/public/gate.html +3 -3
  21. package/modules/hall-ui/public/goal-map.html +3 -3
  22. package/modules/hall-ui/public/goals.html +3 -3
  23. package/modules/hall-ui/public/government.html +3 -3
  24. package/modules/hall-ui/public/idea.html +3 -3
  25. package/modules/hall-ui/public/ideas.html +3 -3
  26. package/modules/hall-ui/public/index.html +3 -3
  27. package/modules/hall-ui/public/modules.html +3 -3
  28. package/modules/hall-ui/public/paintbrush.js +398 -0
  29. package/modules/hall-ui/public/primer.html +3 -3
  30. package/modules/hall-ui/public/profile.html +3 -3
  31. package/modules/hall-ui/public/project-settings.html +3 -3
  32. package/modules/hall-ui/public/ranks.html +3 -3
  33. package/modules/hall-ui/public/roadmap.html +3 -3
  34. package/modules/hall-ui/public/roster.html +3 -3
  35. package/modules/hall-ui/public/sessions.html +3 -3
  36. package/modules/hall-ui/public/settings.html +3 -3
  37. package/modules/hall-ui/public/street.html +3 -3
  38. package/modules/hall-ui/public/studio.html +3 -3
  39. package/modules/hall-ui/public/style.css +78 -0
  40. package/modules/hall-ui/public/task.html +3 -3
  41. package/modules/hall-ui/public/thinking.html +3 -3
  42. package/modules/hall-ui/public/tweak-editor.html +3 -3
  43. package/modules/hall-ui/public/watch.html +3 -3
  44. package/modules/hall-ui/public/work.html +3 -3
  45. package/modules/hall-ui/records/craft-mode.md +9 -0
  46. package/modules/provisioning/manifest-truth.js +63 -0
  47. package/modules/provisioning/provisioning.js +3 -11
  48. package/modules/provisioning/routes/provisioning.js +3 -1
  49. package/modules/public-landing/public/projects.html +38 -11
  50. package/package-lock.json +2 -2
  51. package/package.json +1 -1
  52. package/release-notes.json +12 -0
  53. package/scripts/gds/provision.js +4 -2
  54. package/src/bongos/routes/auth.js +15 -2
  55. package/src/module-api.js +1 -1
  56. package/tests/hall_tweak_editor.mjs +1 -1
  57. package/tests/page_notes_panel.mjs +4 -3
  58. package/tests/paintbrush.mjs +351 -0
  59. package/tests/projects_hub_pre_uat.mjs +3 -2
  60. package/tests/provision.mjs +53 -0
  61. package/tests/provisioning_github_app_routes.mjs +23 -0
  62. package/tests/provisioning_settings.mjs +6 -4
  63. package/tests/wizard_github_feedback.mjs +239 -0
@@ -138,9 +138,9 @@
138
138
  </div>
139
139
  </div>
140
140
 
141
- <script src="/builders/shell-trail.js?v=2026-10-04-nolegacy"></script>
142
- <script src="/builders/craft-mode.js?v=2026-10-04-nolegacy"></script>
143
- <script src="/builders/shell.js?v=2026-10-04-nolegacy"></script>
141
+ <script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
142
+ <script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
143
+ <script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
144
144
  <script src="/builders/palette.js" defer></script>
145
145
 
146
146
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
@@ -142,9 +142,9 @@
142
142
 
143
143
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
144
144
 
145
- <script src="/builders/shell-trail.js?v=2026-10-04-nolegacy"></script>
146
- <script src="/builders/craft-mode.js?v=2026-10-04-nolegacy"></script>
147
- <script src="/builders/shell.js?v=2026-10-04-nolegacy"></script>
145
+ <script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
146
+ <script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
147
+ <script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
148
148
  <script src="/builders/palette.js" defer></script>
149
149
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
150
150
  <script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
@@ -162,9 +162,9 @@
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-trail.js?v=2026-10-04-nolegacy"></script>
166
- <script src="/builders/craft-mode.js?v=2026-10-04-nolegacy"></script>
167
- <script src="/builders/shell.js?v=2026-10-04-nolegacy"></script>
165
+ <script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
166
+ <script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
167
+ <script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
168
168
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
169
169
  <script src="/builders/palette.js" defer></script>
170
170
  <!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
@@ -171,9 +171,9 @@
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-trail.js?v=2026-10-04-nolegacy"></script>
175
- <script src="/builders/craft-mode.js?v=2026-10-04-nolegacy"></script>
176
- <script src="/builders/shell.js?v=2026-10-04-nolegacy"></script>
174
+ <script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
175
+ <script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
176
+ <script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
177
177
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
178
178
  <script src="/builders/palette.js" defer></script>
179
179
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
@@ -35,3 +35,12 @@ Covers: every shelled hall page (`craft-mode.js`, `shell.js`, `style.css`), and
35
35
  - **Two pieces are drawn by `craft-mode.js` `applyLook()`, on every mode change:** the eyebrow beside `#topbar-title` (`LOOKS`: "the studio · night", "the sky", "the city"; hidden at <=640px, where it overflowed phones) and the scene plate under `.app-sidebar__brand` (40px closed rail, 64px open). The plate's markup IS `artHtml(mode)`, the picker's door art, exported rather than copied; light mode recolours it to each scene's day (the city is the dark one through `invert(1) hue-rotate(180deg)`). It does not animate.
36
36
  - **Always-present tabs widened** (owner, 2026-10-03): `ALWAYS_TABS` is Builders, Collab and Profile, so every mode keeps the You and Community groups.
37
37
  - **Governing's header tallies** ("needs you · moving") are task 1004563, not this look.
38
+
39
+ ## The paintbrush (task 1004497)
40
+
41
+ - **Artistry carries two tools**: the palette (a feedback doc, `page-notes.js`, record `page-notes.md`) and the paintbrush (`paintbrush.js`). Both are `TOOLS` entries in `craft-mode.js` and load their own script on first press.
42
+ - **The paintbrush edits the page's own words where they stand, as a REAL wording change** (owner, 2026-10-04): it is the copy desk's page round, the same one the tweak editor and the studio's own words write (ADR 0341). Pressing it reads `GET /copy-desk/pages/builders:<page>/draft`; a page nobody holds is OFFERED, never taken silently (taking it is the copy desk's artists-only claim, and the page shows as being tweaked while held). Typing saves the whole draft; "Review" lists every change as was/now; "Send for approval" submits the round. It never writes `page-notes` (owner: kept separate from the feedback docs).
43
+ - **Only a leaf element inside `<main>` whose words are one reading line becomes editable.** Shared chrome, a line with a `{…}` hole, controls (`button`, form fields, `code`), and anything hidden stay as they are; the bar counts the lines it cannot reach and links to the tweak editor for them. A click on edited words inside a link does not navigate.
44
+ - **Nobody else's view changes, and neither does yours once editing ends**: "Stop editing" and "Send" both put the page's own words back. The draft stays on the round.
45
+ - **The top bar fits two tools at 390px** because, at phone width, the side packs to a 4px gap, a tool is 28px, the side and the menu do not shrink, and a long page title trims with an ellipsis instead. All 34 hall pages measure 0px of sideways scroll in Artistry at 390px.
46
+ - `tests/paintbrush.mjs` runs the real script and both shared libs over a small page.
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ // modules/provisioning/manifest-truth.js — what the wizard is told about the GitHub
4
+ // sign-in leg (the GitHub-App-Manifest handshake, ADR 0133), at every stage.
5
+ //
6
+ // WHY (task 1004572, live on 2026-10-04). A founder setting up a project saw
7
+ // "Waiting for your approval on GitHub…" while the platform was already placing
8
+ // credentials, then "GitHub setup didn't start. Nothing was changed on GitHub" after
9
+ // GitHub HAD created an app on their account, and — once a setup link lapsed — no
10
+ // button at all, forever, because nothing ever flips a lapsed 'pending' row.
11
+ // These helpers make the read surface say what is actually true. PURE: no DB, no
12
+ // network; split out because provisioning.js sits at the 1500-line ratchet.
13
+
14
+ // The status a manifest row MEANS right now. Only 'pending' decays: past expires_at
15
+ // the callback refuses its code (attachManifestCode's TTL guard in SQL), so a lapsed
16
+ // 'pending' row is expired in every sense but the column.
17
+ function effectiveManifestStatus(row, now = Date.now()) {
18
+ const status = String((row && row.status) || '');
19
+ if (status === 'pending' && row.expires_at && new Date(row.expires_at).getTime() <= now) return 'expired';
20
+ return status;
21
+ }
22
+
23
+ // How far a FAILED attempt got, as a closed vocabulary the wizard turns into a
24
+ // sentence. The raw last_error never leaves the server (the status route is public
25
+ // by state token). Read from the row's shape, never from error-message text:
26
+ // 'not_approved' — the runner never picked it up (attempts 0): nothing we
27
+ // know of happened on GitHub's side.
28
+ // 'sign_in_unconfirmed' — GitHub handed over the credentials (the App's slug is
29
+ // recorded) and they were placed, but the project never
30
+ // confirmed sign-in works. An App exists on the founder's
31
+ // account, so "try again" makes a second one.
32
+ // 'finish_failed' — GitHub approved, but turning that into credentials on
33
+ // the project failed before the App was recorded.
34
+ const MANIFEST_FAILURES = Object.freeze(['not_approved', 'sign_in_unconfirmed', 'finish_failed']);
35
+
36
+ function manifestFailure(row) {
37
+ if (!row || !(Number(row.attempts) > 0)) return 'not_approved';
38
+ if (row.app_slug) return 'sign_in_unconfirmed';
39
+ return 'finish_failed';
40
+ }
41
+
42
+ // The GitHub-App manifest leg's truth for an instance, as the instance read carries
43
+ // it (task 1003235): the latest attempt's lifecycle status + the App's public slug,
44
+ // in the same vocabulary as GET /github-app/status ('pending' | 'code_received' |
45
+ // 'converting' | 'done' | 'error' | 'expired'), so the wizard can seed `live.manifest`
46
+ // after a reload, a second tab, or another device. null = no attempt ever made.
47
+ // status — the EFFECTIVE status (above).
48
+ // failure — on 'error' only, how far it got (above); null otherwise.
49
+ // started_at — when the setup link was minted, so a reloaded page can still offer
50
+ // "start again" once the wait has clearly gone wrong.
51
+ // An allow-list projection of the row, never the row itself (it holds GitHub's code).
52
+ function publicGithubApp(manifestRow, now = Date.now()) {
53
+ if (!manifestRow || !manifestRow.status) return null;
54
+ const status = effectiveManifestStatus(manifestRow, now);
55
+ return {
56
+ status,
57
+ app_slug: manifestRow.app_slug || null,
58
+ failure: status === 'error' ? manifestFailure(manifestRow) : null,
59
+ started_at: manifestRow.created_at ? new Date(manifestRow.created_at).toISOString() : null,
60
+ };
61
+ }
62
+
63
+ module.exports = { effectiveManifestStatus, manifestFailure, publicGithubApp, MANIFEST_FAILURES };
@@ -477,17 +477,9 @@ function publicInstance(row) {
477
477
  };
478
478
  }
479
479
 
480
- // The GitHub-App manifest leg's truth for an instance, as the read surface carries
481
- // it (task 1003235): the latest attempt's lifecycle status + the App's public slug.
482
- // Same vocabulary as GET /github-app/status ('pending' | 'code_received' |
483
- // 'converting' | 'done' | 'error' | 'expired'), so the wizard can seed its
484
- // `live.manifest` from a plain instance read after a reload, a second tab, or
485
- // another device — no sessionStorage bridge needed. null = no attempt ever made.
486
- // PURE: an allow-list projection of the manifest row, never the row itself.
487
- function publicGithubApp(manifestRow) {
488
- if (!manifestRow || !manifestRow.status) return null;
489
- return { status: String(manifestRow.status), app_slug: manifestRow.app_slug || null };
490
- }
480
+ // The GitHub-App manifest leg's truth as the read surface carries it (task 1003235;
481
+ // task 1004572 made it tell the truth at every stage) — moved to manifest-truth.js.
482
+ const { publicGithubApp } = require('./manifest-truth');
491
483
 
492
484
  // The env variables that carry this instance's settings, as the instance reads
493
485
  // them: { <PREFIX>_PLATFORM_VISIBILITY, <PREFIX>_JOINABILITY } (task 1003139 /
@@ -1405,10 +1405,12 @@ module.exports = function provisioningRoutes() {
1405
1405
  const row = await provisioning.getOAuthManifestByState(pool, state);
1406
1406
  if (!row) return res.status(404).json({ ok: false, error: 'unknown_state' });
1407
1407
  const inst = await provisioning.getInstanceById(pool, row.instance_id).catch(() => null);
1408
- const status = row.status;
1408
+ // EFFECTIVE status + how far a failure got (task 1004572, manifest-truth.js)
1409
+ const { status, failure } = provisioning.publicGithubApp(row);
1409
1410
  return res.json({
1410
1411
  ok: true,
1411
1412
  status,
1413
+ failure,
1412
1414
  ready: status === 'done',
1413
1415
  terminal: status === 'done' || status === 'error' || status === 'expired',
1414
1416
  domain: inst && inst.domain ? inst.domain : null,
@@ -6533,7 +6533,7 @@ summary{min-height:24px;padding:3px 0;}
6533
6533
  return '<a href="' + (instanceId ? '?view=manage&id=' + encodeURIComponent(instanceId) : '?view=mine') +
6534
6534
  '">' + label + '</a>';
6535
6535
  }
6536
- var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', appSlug: null };
6536
+ var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null };
6537
6537
  var planPromises = {}; /* mode|domain → Promise<steps[]> */
6538
6538
  var lastStepsHtml = '';
6539
6539
  var lastProgHtml = ''; /* same change-only contract, for the progress strip */
@@ -6654,14 +6654,24 @@ summary{min-height:24px;padding:3px 0;}
6654
6654
  details (task 1003244). The one refusal a founder can act on — no
6655
6655
  address yet — says where to go instead of what the server said. */
6656
6656
  var snag, raw = '';
6657
+ /* a failure AFTER GitHub's approval left an app on the founder's account,
6658
+ so "nothing was changed" would be false — the server says how far it got
6659
+ (live.manifestFailure, task 1004572) and the words follow it */
6657
6660
  if (live.manifest === 'expired') snag = 'That setup link expired (they last 30 minutes) — mint a fresh one.';
6658
6661
  else if (live.manifestCode === 'domain_required') snag = 'This project needs an address first — attach one from ' + projectPageLink('its own page') + ', then come back to this step.';
6662
+ else if (live.manifestFailure === 'sign_in_unconfirmed') {
6663
+ var appLink = live.appSlug ? '<a href="https://github.com/settings/apps/' + esc(live.appSlug) + '" target="_blank" rel="noopener">your app on GitHub</a>' : 'the app on your GitHub account';
6664
+ return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>GitHub approved the app and the platform placed its credentials on your project, but your project didn’t confirm that sign-in works. Trying again would make a second app on your GitHub account and is unlikely to fix this — it needs a look from the platform team. <a href="/contact">Contact us</a> and mention ' + appLink + '.</p>' +
6665
+ progressMarkup(1, 1, 'Stopped: your project didn’t confirm sign-in.', 'isErr', 'signin') +
6666
+ '<button type="button" class="linklike" data-action="manifest-start">Try again anyway</button></li>';
6667
+ }
6668
+ else if (live.manifestFailure === 'finish_failed') snag = 'GitHub approved the app, but the platform couldn’t finish setting up sign-in on your project. The app GitHub made is still on your account — delete it first (GitHub → Settings → Developer settings → GitHub Apps) so you don’t end up with two, then try again.';
6659
6669
  else { snag = 'GitHub setup didn’t start. Nothing was changed on GitHub — try again.'; raw = live.manifestMsg; }
6660
6670
  return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>' + snag + '</p>' +
6661
6671
  (raw ? '<details><summary>Technical details</summary><p>' + esc(raw) + '</p></details>' : '') +
6662
6672
  '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
6663
6673
  }
6664
- if (live.manifest === 'code_received') {
6674
+ if (live.manifest === 'code_received' || live.manifest === 'converting') {
6665
6675
  /* GitHub's half is done — say so, rather than keep telling the founder
6666
6676
  to approve an app they just approved */
6667
6677
  autoOpenArmed = true;
@@ -6670,7 +6680,7 @@ summary{min-height:24px;padding:3px 0;}
6670
6680
  }
6671
6681
  if (live.manifest) { /* pending — GitHub holds the manifest, nobody has approved it yet */
6672
6682
  autoOpenArmed = true;
6673
- return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>Waiting on GitHub — approve the app in the tab that opened. Once approved, the platform places the credentials and verifies sign-in on its own; this step ticks itself.</p>' +
6683
+ return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>Waiting on GitHub — approve the app in the tab that opened. No GitHub tab? It may have opened behind this window or in another one. Once approved, the platform places the credentials and verifies sign-in on its own; this step ticks itself.</p>' +
6674
6684
  progressMarkup(0, 0, 'Waiting for your approval on GitHub…', '', 'signin') + autoOpenTick() +
6675
6685
  (manifestStale ? '<button type="button" class="linklike" data-action="manifest-start">Didn’t get to GitHub? Start the setup again</button>' : '') + '</li>';
6676
6686
  }
@@ -6889,7 +6899,7 @@ summary{min-height:24px;padding:3px 0;}
6889
6899
  inp.type = 'hidden'; inp.name = 'manifest'; inp.value = JSON.stringify(d.manifest);
6890
6900
  form.appendChild(inp); document.body.appendChild(form);
6891
6901
  form.submit(); form.remove();
6892
- live.manifest = 'pending'; live.manifestMsg = '';
6902
+ live.manifest = 'pending'; live.manifestMsg = ''; live.manifestFailure = null;
6893
6903
  manifestStartedAt = Date.now(); manifestStale = false;
6894
6904
  rememberManifest(d.state, 'pending');
6895
6905
  renderSteps();
@@ -6897,12 +6907,21 @@ summary{min-height:24px;padding:3px 0;}
6897
6907
  }).catch(function (err) {
6898
6908
  if (String(err && err.message) === 'signing in') return;
6899
6909
  if (tab) { try { tab.close(); } catch (e) {} }
6900
- live.manifest = 'error'; live.manifestMsg = String((err && err.message) || 'could not start');
6910
+ live.manifest = 'error'; live.manifestFailure = null; live.manifestMsg = String((err && err.message) || 'could not start');
6901
6911
  live.manifestCode = String((err && err.code) || '');
6902
6912
  renderSteps();
6903
6913
  });
6904
6914
  }
6905
6915
 
6916
+ /* the "start again" offer once a wait has clearly gone wrong — measured from
6917
+ when the link was MINTED (the server's started_at after a reload), so a
6918
+ reloaded page offers it too instead of never (task 1004572) */
6919
+ function manifestInFlight() {
6920
+ return live.manifest === 'pending' || live.manifest === 'code_received' || live.manifest === 'converting';
6921
+ }
6922
+ function noteManifestWait() {
6923
+ if (live.manifest === 'pending' && !manifestStale && manifestStartedAt && (Date.now() - manifestStartedAt) > 90000) { manifestStale = true; renderSteps(); }
6924
+ }
6906
6925
  function pollManifest(stateToken) {
6907
6926
  stopManifestPoll();
6908
6927
  manifestState = stateToken;
@@ -6916,8 +6935,9 @@ summary{min-height:24px;padding:3px 0;}
6916
6935
  if (myGen !== manifestGen) return;
6917
6936
  if (d.app_slug && d.app_slug !== live.appSlug) { live.appSlug = d.app_slug; rememberManifest(undefined, undefined, d.app_slug); renderSteps(); }
6918
6937
  var next = d.ready ? 'done' : (d.status || 'pending');
6919
- if (next !== live.manifest) { live.manifest = next; rememberManifest(undefined, next); renderSteps(); }
6920
- else if (next === 'pending' && !manifestStale && manifestStartedAt && (Date.now() - manifestStartedAt) > 90000) { manifestStale = true; renderSteps(); }
6938
+ var failure = d.failure || null;
6939
+ if (next !== live.manifest || failure !== live.manifestFailure) { live.manifest = next; live.manifestFailure = failure; rememberManifest(undefined, next); renderSteps(); }
6940
+ else noteManifestWait();
6921
6941
  if (!d.terminal) manifestPollTimer = setTimeout(tick, 4000);
6922
6942
  else manifestPollTimer = null; /* a stale fired id would read as "still polling" and resume a finished poll */
6923
6943
  })
@@ -7119,10 +7139,15 @@ summary{min-height:24px;padding:3px 0;}
7119
7139
  var ga = data.github_app;
7120
7140
  if (ga && ga.status && !manifestPollTimer) {
7121
7141
  var seeded = ga.status === 'done' ? 'done' : ga.status;
7122
- var seedChanged = seeded !== live.manifest || (!!ga.app_slug && ga.app_slug !== live.appSlug);
7142
+ var seedFailure = ga.failure || null;
7143
+ var seedChanged = seeded !== live.manifest || seedFailure !== live.manifestFailure || (!!ga.app_slug && ga.app_slug !== live.appSlug);
7123
7144
  if (ga.app_slug) live.appSlug = ga.app_slug;
7124
- live.manifest = seeded;
7145
+ live.manifest = seeded; live.manifestFailure = seedFailure;
7146
+ /* the wait is timed from when the link was MINTED, so a reload still
7147
+ reaches the "start again" offer (task 1004572) */
7148
+ if (!manifestStartedAt && ga.started_at) manifestStartedAt = Date.parse(ga.started_at) || 0;
7125
7149
  if (seedChanged) { rememberManifest(undefined, live.manifest, live.appSlug || undefined); renderSteps(); }
7150
+ else noteManifestWait();
7126
7151
  }
7127
7152
  /* the checklist reflects live state (task 1002694): a status transition —
7128
7153
  e.g. the scaffold landing — re-renders the steps (change-only, so the
@@ -7131,7 +7156,9 @@ summary{min-height:24px;padding:3px 0;}
7131
7156
  live.instStatus = inst.status; live.addrReady = addressReady(inst); renderSteps();
7132
7157
  }
7133
7158
  var terminal = renderProgress(inst);
7134
- if (!terminal) pollTimer = setTimeout(tick, 5000);
7159
+ /* a GitHub leg still in flight keeps this read alive even once the
7160
+ project is up — after a reload it is the ONLY thing watching it */
7161
+ if (!terminal || manifestInFlight()) pollTimer = setTimeout(tick, 5000);
7135
7162
  else stopPolling();
7136
7163
  })
7137
7164
  .catch(function (err) {
@@ -7160,7 +7187,7 @@ summary{min-height:24px;padding:3px 0;}
7160
7187
  instanceId = null;
7161
7188
  watchPending = false; manifestPending = false; manifestState = null;
7162
7189
  manifestStartedAt = 0; manifestStale = false;
7163
- live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', appSlug: null };
7190
+ live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null };
7164
7191
  lastStepsHtml = '';
7165
7192
  lastProgHtml = '';
7166
7193
  try { sessionStorage.removeItem(DRAFT_KEY); } catch (e) {}
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.44",
3
+ "version": "1.21.46",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.44",
9
+ "version": "1.21.46",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.44",
3
+ "version": "1.21.46",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -9028,5 +9028,17 @@
9028
9028
  "id": "1004569",
9029
9029
  "text": "The street's pins no longer pile up or talk over each other. Each building now has one pin, and its colour and size show how much it matters: largest and pulsing orange when something needs you within a day, orange when it nee"
9030
9030
  }
9031
+ ],
9032
+ "1.21.45": [
9033
+ {
9034
+ "id": "1004497",
9035
+ "text": "Artists can now edit any hall page's words right where they stand. In Artistry mode, \"Edit words\" offers to take the page; the words get an underline and can be typed over, a review lists each change as was/now, and \"Send for"
9036
+ }
9037
+ ],
9038
+ "1.21.46": [
9039
+ {
9040
+ "id": "1004572",
9041
+ "text": "Setting up GitHub sign-in for a new project now says what is really happening at each step, and always leaves a way forward if it gets stuck."
9042
+ }
9031
9043
  ]
9032
9044
  }
@@ -1095,7 +1095,9 @@ async function placeManifestCreds(inst, mrow, deps) {
1095
1095
  await sleep(MANIFEST_VERIFY_INTERVAL_MS);
1096
1096
  }
1097
1097
  if (!authConfigured) {
1098
- return { ok: false, codeSpent: true, error: 'the credentials are placed, but the project did not report sign-in ready in time' };
1098
+ // The App now EXISTS on the owner's account — keep its id + slug on the row so
1099
+ // the wizard can name it and say why "try again" makes a second one (task 1004572).
1100
+ return { ok: false, codeSpent: true, appId: creds.appId, appSlug: creds.appSlug, error: 'the credentials are placed, but the project did not report sign-in ready in time' };
1099
1101
  }
1100
1102
  log(' [oauth-manifest] ✓ /healthz auth_configured=true — sign-in is live');
1101
1103
  } else {
@@ -1147,7 +1149,7 @@ async function drainOAuthManifests(deps) {
1147
1149
  // single-use, so resetting the row to 'code_received' guarantees the
1148
1150
  // next tick re-converts a burned code and 404s forever. Only failures
1149
1151
  // that happened BEFORE the conversion stay retryable.
1150
- if (result.codeSpent) await provisioning.resolveOAuthManifest(db, mrow.id, 'error', { last_error: result.error, github_code: null });
1152
+ if (result.codeSpent) await provisioning.resolveOAuthManifest(db, mrow.id, 'error', { last_error: result.error, github_code: null, ...(result.appSlug ? { app_id: result.appId, app_slug: result.appSlug } : {}) });
1151
1153
  else if (mrow.attempts >= MAX_INTENT_ATTEMPTS) await provisioning.resolveOAuthManifest(db, mrow.id, 'error', { last_error: result.error });
1152
1154
  else await provisioning.resolveOAuthManifest(db, mrow.id, 'code_received', { last_error: result.error });
1153
1155
  errors++; log(` ✖ oauth-manifest #${mrow.id}: ${result.error}`);
@@ -127,6 +127,19 @@ function renderAuthErrorPage(heading, detail, opts = {}) {
127
127
  </body></html>`;
128
128
  }
129
129
 
130
+ // The project has no way to sign anyone in yet: neither its own GitHub app nor
131
+ // sign-in through the platform (task 1004572). It used to answer a bare 503
132
+ // "auth not configured", which told a founder mid-setup nothing they could act on.
133
+ // Still a 503 — the service genuinely is not available — but in words, and
134
+ // pointing the one person who can fix it at the step that does.
135
+ const AUTH_NOT_CONFIGURED_DETAIL = "Sign-in isn't set up on this project yet. If it's your project, go back to its setup checklist on the platform where you created it and finish the \"Wire GitHub sign-in\" step. Otherwise, ask the project's owner.";
136
+ function sendAuthNotConfigured(res) {
137
+ return res.status(503).type('text/html').send(renderAuthErrorPage(
138
+ "Sign-in isn't set up yet", AUTH_NOT_CONFIGURED_DETAIL,
139
+ { subtitle: "This project can't sign anyone in yet.", cta: { href: '/', label: 'Back to the project' }, note: 'Nothing is wrong with your account.' },
140
+ ));
141
+ }
142
+
130
143
  // renderAccessPendingPage — the calm "hang tight" page shown instead of the red
131
144
  // dead-end when admission is plausibly imminent (task 1002097): the configured
132
145
  // firstAdmin during the bootstrap window, or a login with a pending request.
@@ -681,7 +694,7 @@ module.exports = function buildAuthRouter() {
681
694
  // Either direct-GitHub creds OR platform-IdP federation must be configured
682
695
  // (ADR 0141 §1) — a federated instance has no GitHub app of its own.
683
696
  if (!auth.authConfigured() && !auth.idpConfigured()) {
684
- return res.status(503).type('text/plain').send('auth not configured');
697
+ return sendAuthNotConfigured(res);
685
698
  }
686
699
  const wantsRepos = req.query.repos === '1' || req.query.repos === 'true';
687
700
  // A sign-in begun on a satellite hub host (builders./status.) restarts on
@@ -842,7 +855,7 @@ module.exports = function buildAuthRouter() {
842
855
  // rank: public — OAuth callback; the state cookie + code are the bearer.
843
856
  router.get('/auth/web/callback', async (req, res) => {
844
857
  if (!auth.authConfigured() && !auth.idpConfigured()) {
845
- return res.status(503).type('text/plain').send('auth not configured');
858
+ return sendAuthNotConfigured(res);
846
859
  }
847
860
  const expectedState = parseCookie(req, STATE_COOKIE) || parseCookie(req, STATE_COOKIE_LEGACY);
848
861
  const { state, code, error } = req.query;
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.44'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.46'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -635,7 +635,7 @@ try {
635
635
  assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
636
636
  assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
637
637
  assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
638
- assert.match(HTML, /shell\.js\?v=2026-10-04-nolegacy/);
638
+ assert.match(HTML, /shell\.js\?v=2026-10-04-paintbrush/);
639
639
  assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
640
640
  for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
641
641
  assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
@@ -76,8 +76,9 @@ const flush = () => new Promise((r) => setTimeout(r, 0));
76
76
 
77
77
  test('THE PALETTE: in the top bar in Artistry alone, right before the switch, and gone when the mode changes', () => {
78
78
  const p = boot({ session: { 'hall-craft': 'artistry' } });
79
- assert.deepEqual(p.tools().map((t) => t.id), ['craft-tool-palette']);
80
- assert.equal(p.tools()[0].nextSibling, p.sw, 'it sits right before the mode switch');
79
+ assert.deepEqual(p.tools().map((t) => t.id), ['craft-tool-palette', 'craft-tool-paintbrush'], 'the palette, then the paintbrush (task 1004497)');
80
+ assert.equal(p.tools()[0].nextSibling, p.tools()[1], 'the palette comes first');
81
+ assert.equal(p.tools()[1].nextSibling, p.sw, 'and the pair sits right before the mode switch');
81
82
  assert.equal(p.tools()[0].getAttribute('aria-label'), 'Write feedback on this page');
82
83
  for (const m of ['building', 'ideating', 'governing']) {
83
84
  p.craft.choose(m);
@@ -85,7 +86,7 @@ test('THE PALETTE: in the top bar in Artistry alone, right before the switch, an
85
86
  }
86
87
  p.craft.choose('artistry');
87
88
  p.craft.choose('artistry');
88
- assert.equal(p.tools().length, 1, 'choosing Artistry twice draws one palette');
89
+ assert.equal(p.tools().length, 2, 'choosing Artistry twice draws one palette and one paintbrush');
89
90
  assert.equal(boot().tools().length, 0, 'and none with no mode at all');
90
91
  });
91
92