@bongos/core 1.19.66 → 1.19.68

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.
package/.bongos-core.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.66",
6
- "core_contract": "1.19.66",
7
- "source_commit": "b7a4a393e851903e595719125e64e6c5abafceb4",
5
+ "core_version": "1.19.68",
6
+ "core_contract": "1.19.68",
7
+ "source_commit": "0b0e7e636f0a28d01e1bc8f354a06981d6f1ef70",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-12T13:10:03.862Z",
9
+ "built_at": "2026-08-12T14:42:16.480Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 292,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 1535,
20
- "tree_sha256": "de43bd7bdf0e7964a2f861fc87f75fa4bd1ae0e1110f12b37486ce766c33c2a3",
20
+ "tree_sha256": "cc9278ac9df146953fa33e771313791e8ce82f5d69b05a307763c48bae6207e9",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -187,7 +187,7 @@
187
187
  {
188
188
  "path": ".claude/skills/new-project/SKILL.md",
189
189
  "mode": "0000644",
190
- "sha256": "4bde7c3a6792e3df0490277095afb061435b921b3b7934d6519e3b3115095710"
190
+ "sha256": "55b86978a60e33eefe024d7c7d521e4093b82235eeb7e5d4a145fbb0e4db0b06"
191
191
  },
192
192
  {
193
193
  "path": ".claude/skills/otb-character-review/SKILL.md",
@@ -1312,12 +1312,12 @@
1312
1312
  {
1313
1313
  "path": "docs/api-reference.md",
1314
1314
  "mode": "0000644",
1315
- "sha256": "a30007778606ce90b8e31470d926047a372c72b93022f54023937cfd4c5b5fc0"
1315
+ "sha256": "1a4562c2e0575f8505a22851c63f1cc3ecfd485ae5de6e725810786f692e6457"
1316
1316
  },
1317
1317
  {
1318
1318
  "path": "docs/api/openapi.json",
1319
1319
  "mode": "0000644",
1320
- "sha256": "0e7646ee148453ef5d6d51b3434253b76f35628ee6c36681dbf6e7b27dd20c6f"
1320
+ "sha256": "9a551d5c62209f2c1b935194cc37e86ca5c44b317901499da84126e3868df1fa"
1321
1321
  },
1322
1322
  {
1323
1323
  "path": "docs/architecture.md",
@@ -3737,7 +3737,7 @@
3737
3737
  {
3738
3738
  "path": "modules/provisioning/onboard-plan.js",
3739
3739
  "mode": "0000644",
3740
- "sha256": "edee29c46f4f106ac08b9b048769182a1d63390a5621bbb6a83361e64a000a4e"
3740
+ "sha256": "19224b44f22a18db651cfefa87c90c250e911dc1b1d00b76ed7c9361b505c037"
3741
3741
  },
3742
3742
  {
3743
3743
  "path": "modules/provisioning/provisioning.js",
@@ -3752,7 +3752,7 @@
3752
3752
  {
3753
3753
  "path": "modules/provisioning/routes/provisioning.js",
3754
3754
  "mode": "0000644",
3755
- "sha256": "a490a4b4b607a302533bac6fedbc39a0153d57e35a086bcc20079fbefc6815e5"
3755
+ "sha256": "a8e2fa66eb19d119e117b80ca49832bec240cb71d167719e2a86f0032823b506"
3756
3756
  },
3757
3757
  {
3758
3758
  "path": "modules/provisioning/tests/provisioning.mjs",
@@ -4042,12 +4042,12 @@
4042
4042
  {
4043
4043
  "path": "package-lock.json",
4044
4044
  "mode": "0000644",
4045
- "sha256": "c143dc9d0d61ff73fc5d95421a08b76dd695932c371b2c75f6383a4dfa50e641"
4045
+ "sha256": "9319b9acd693e6a9804c215db5ed78ae38f4caf1a698e0596972bfef9555b91c"
4046
4046
  },
4047
4047
  {
4048
4048
  "path": "package.json",
4049
4049
  "mode": "0000644",
4050
- "sha256": "cadc0c2dd9db11ec051eddb312cfbb7433a7e3cd88bfaa18ea98180351d8032a"
4050
+ "sha256": "147790e86972f4273aa18bd24bf508797c4470a4491a20a9614207edeb2e3478"
4051
4051
  },
4052
4052
  {
4053
4053
  "path": "public-docs/index.html",
@@ -4292,7 +4292,7 @@
4292
4292
  {
4293
4293
  "path": "public-landing/create.html",
4294
4294
  "mode": "0000644",
4295
- "sha256": "0333bd3cea25e22fc6ad70097a4a57e1fa779b6d47156b551782a06846f76c0b"
4295
+ "sha256": "1874575c2241df1e868d158f3fc4c6c123b8c35531a5b1425de9974230da6510"
4296
4296
  },
4297
4297
  {
4298
4298
  "path": "public-landing/gate/README.md",
@@ -4977,7 +4977,7 @@
4977
4977
  {
4978
4978
  "path": "scripts/gds/onboard.js",
4979
4979
  "mode": "0000644",
4980
- "sha256": "c1f8299289e917a04f685d72dbced1778f5888aac53bb7ffe4bed7f9ad453b42"
4980
+ "sha256": "11472bb450751330e865cbd8f6dba0276005f120db6438e058cd148dbeb580fb"
4981
4981
  },
4982
4982
  {
4983
4983
  "path": "scripts/gds/onboarding-analyze.js",
@@ -5672,7 +5672,7 @@
5672
5672
  {
5673
5673
  "path": "src/module-api.js",
5674
5674
  "mode": "0000644",
5675
- "sha256": "8dd5e994caef229a93a1ef8fd90e20e24ff9bd7a3ed9ba27bffe5732cfdf5960"
5675
+ "sha256": "28675b76775e2936b6ba819fa37c6628f96b259b850248f2ce1033e9bcd7fe20"
5676
5676
  },
5677
5677
  {
5678
5678
  "path": "src/module-loader/loader.js",
@@ -7037,7 +7037,7 @@
7037
7037
  {
7038
7038
  "path": "tests/onboard.mjs",
7039
7039
  "mode": "0000644",
7040
- "sha256": "61f445855391bfc0e4ab44f8a3f03e074d911e75ed7dbae31293f38314cd3d2e"
7040
+ "sha256": "6a66477263031b252d6a92d5036304a78e11cbbd9e7230d836c4ec96a9297fdd"
7041
7041
  },
7042
7042
  {
7043
7043
  "path": "tests/onboarding_gate.mjs",
@@ -7167,7 +7167,7 @@
7167
7167
  {
7168
7168
  "path": "tests/provisioning_onboard_mode.mjs",
7169
7169
  "mode": "0000644",
7170
- "sha256": "9a41d64fba3a8cc8ffdbccb0aea363ca6ccb63909cfeefd4bd0b05ebab052403"
7170
+ "sha256": "7749df454ac3cca7c7e5ea1acc94ead15d1c9f87b38fe2028a1c93c8adf31bcf"
7171
7171
  },
7172
7172
  {
7173
7173
  "path": "tests/provisioning_teardown_intent.mjs",
@@ -99,6 +99,13 @@ Confirm the owner is **Archon** (their profile / the hall roster). If `auth_conf
99
99
 
100
100
  Fill the instance **look** — theme / palette / fonts in `config/branding.json` (see `docs/branding-fill-prompt.md`). The identity strings were set at init; this is the visual layer. "Brand with your own LLM."
101
101
 
102
+ ### 9. Enter your builders hall
103
+
104
+ The hall is the point of everything above — end there, not at brand (task 1002703):
105
+
106
+ - **With a domain:** open `https://<domain>/builders` — that's the project's home.
107
+ - **No domain yet:** run `bongos dev` in the repo checkout — it stands the instance up locally and serves the hall on the owner's machine ([ADR 0149](../../../docs/adr/<redacted>.md)).
108
+
102
109
  ## Done-when
103
110
 
104
111
  A new owner has gone zero → live: their own standalone repo on a pinned core, provisioned with no hand-editing of servers, the four human-only steps done UI-first + verified, and they are signed in as the founding Archon on their own domain. That is goal 35's `project-creation-flow` criterion.
@@ -8733,7 +8733,7 @@
8733
8733
  "provisioning"
8734
8734
  ],
8735
8735
  "summary": "GET /provisioning/onboard-plan",
8736
- "description": "GET /provisioning/onboard-plan?mode=greenfield|adopt — the CANONICAL onboarding step sequence (task 2085; goal 35 / criterion 165: \"the canonical step sequence is the source of truth; skill/CLI/hall are forms over it\"). The branded /create page + the hall wizard fetch it so their human-steps checklist can't drift from the CLI's (scripts/gds/onboard.js reads the same modules/provisioning/onboard-plan.js). rank: public — the signed-out /create landing page reads it; it is the same public runbook the /new-project skill documents (no write, no secret).\n\n**Rank:** `public` — No authentication — any caller.",
8736
+ "description": "GET /provisioning/onboard-plan?mode=greenfield|adopt&domain=<host> — the CANONICAL onboarding step sequence (task 2085; goal 35 / criterion 165: \"the canonical step sequence is the source of truth; skill/CLI/hall are forms over it\"). The branded /create page + the hall wizard fetch it so their human-steps checklist can't drift from the CLI's (scripts/gds/onboard.js reads the same modules/provisioning/onboard-plan.js). `domain` relabels the terminal enter-hall step with the instance's own hall address (task 1002703); a non-hostname is dropped — clients render these labels, so unvalidated input must never be echoed back. rank: public — the signed-out /create landing page reads it; it is the same public runbook the /new-project skill documents (no write, no secret).\n\n**Rank:** `public` — No authentication — any caller.",
8737
8737
  "x-rank": "public",
8738
8738
  "x-source": "modules/provisioning/routes/provisioning.js",
8739
8739
  "responses": {
@@ -374,7 +374,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
374
374
  | POST | `/api/bongos/provisioning/instances/:id/force-teardown` | `metic+archon` | — | POST /provisioning/instances/:id/force-teardown — Archon tears down ANY instance. |
375
375
  | POST | `/api/bongos/provisioning/instances/:id/github-app` | `any-builder` | `account` | POST /provisioning/instances/:id/github-app — mint a GitHub-App-Manifest onboarding link for the caller's OWN instance (task 2080, ADR 01… |
376
376
  | POST | `/api/bongos/provisioning/instances/:id/teardown` | `any-builder` | — | POST /provisioning/instances/:id/teardown — request teardown of the caller's OWN instance. |
377
- | GET | `/api/bongos/provisioning/onboard-plan` | `public` | — | GET /provisioning/onboard-plan?mode=greenfield\|adopt — the CANONICAL onboarding step sequence (task 2085; goal 35 / criterion 165: "the c… |
377
+ | GET | `/api/bongos/provisioning/onboard-plan` | `public` | — | GET /provisioning/onboard-plan?mode=greenfield\|adopt&domain=<host> — the CANONICAL onboarding step sequence (task 2085; goal 35 / criteri… |
378
378
  | GET | `/api/bongos/provisioning/tls-check` | `public` | — | GET /provisioning/tls-check?domain=<host> — the Caddy on-demand-TLS ALLOW-LIST (ADR 0111 §4, task P4 task 1945). |
379
379
 
380
380
  ## `public` (18)
@@ -11,13 +11,19 @@
11
11
  //
12
12
  // Each step: { key, kind: 'auto' | 'human', label }. `kind: 'human'` marks the
13
13
  // irreducibly-manual owner actions (create the GitHub repo, wire GitHub sign-in, first
14
- // sign-in) that every form renders UI-first; `kind: 'auto'` steps the flow performs.
15
- // The sequence branches at exactly ONE leg — scaffold — into greenfield (`bongos init`,
16
- // a fresh empty repo) vs brownfield (`bongos init --adopt`, LAYER onto an existing repo;
17
- // ADR 0121 §Decision 1 / task 2046). Every other leg is identical, so `mode` only
18
- // relabels the github-repo / init / scaffold steps.
19
- function onboardPlan(mode = 'greenfield') {
14
+ // sign-in, enter the hall) that every form renders UI-first; `kind: 'auto'` steps the
15
+ // flow performs. The sequence branches at exactly ONE leg — scaffold — into greenfield
16
+ // (`bongos init`, a fresh empty repo) vs brownfield (`bongos init --adopt`, LAYER onto
17
+ // an existing repo; ADR 0121 §Decision 1 / task 2046). Every other leg is identical, so
18
+ // `mode` only relabels the github-repo / init / scaffold steps.
19
+ //
20
+ // `opts.domain` (task 1002703): the terminal enter-hall step names the instance's own
21
+ // hall address when the renderer knows the domain, and falls back to the `bongos dev`
22
+ // local-hall one-liner when it doesn't — the hall is the point of creating a project,
23
+ // so the sequence must end there, not at brand.
24
+ function onboardPlan(mode = 'greenfield', opts = {}) {
20
25
  const adopt = mode === 'adopt';
26
+ const domain = typeof opts.domain === 'string' && opts.domain.trim() ? opts.domain.trim() : null;
21
27
  return [
22
28
  { key: 'name-address', kind: 'auto', label: 'Decide name + address (interview)' },
23
29
  { key: 'github-repo', kind: 'human', label: adopt ? 'Use your EXISTING GitHub repository' : 'Create the empty GitHub repository' },
@@ -31,6 +37,9 @@ function onboardPlan(mode = 'greenfield') {
31
37
  { key: 'first-sign-in', kind: 'human', label: 'First sign-in (seats the founding Archon)' },
32
38
  { key: 'verify', kind: 'auto', label: 'Verify /healthz auth_configured' },
33
39
  { key: 'brand', kind: 'auto', label: 'Brand the instance look' },
40
+ { key: 'enter-hall', kind: 'human', label: domain
41
+ ? `Enter your builders hall — https://${domain}/builders`
42
+ : 'Enter your builders hall — run `bongos dev` for the local hall (no domain attached yet)' },
34
43
  ];
35
44
  }
36
45
 
@@ -609,16 +609,20 @@ module.exports = function provisioningRoutes() {
609
609
  res.json({ module: 'provisioning', ok: true, coreVersion: api.CORE_VERSION });
610
610
  });
611
611
 
612
- // GET /provisioning/onboard-plan?mode=greenfield|adopt — the CANONICAL onboarding
613
- // step sequence (task 2085; goal 35 / criterion 165: "the canonical step sequence is
614
- // the source of truth; skill/CLI/hall are forms over it"). The branded /create page +
615
- // the hall wizard fetch it so their human-steps checklist can't drift from the CLI's
616
- // (scripts/gds/onboard.js reads the same modules/provisioning/onboard-plan.js).
612
+ // GET /provisioning/onboard-plan?mode=greenfield|adopt&domain=<host> — the CANONICAL
613
+ // onboarding step sequence (task 2085; goal 35 / criterion 165: "the canonical step
614
+ // sequence is the source of truth; skill/CLI/hall are forms over it"). The branded
615
+ // /create page + the hall wizard fetch it so their human-steps checklist can't drift
616
+ // from the CLI's (scripts/gds/onboard.js reads the same
617
+ // modules/provisioning/onboard-plan.js). `domain` relabels the terminal enter-hall
618
+ // step with the instance's own hall address (task 1002703); a non-hostname is dropped
619
+ // — clients render these labels, so unvalidated input must never be echoed back.
617
620
  // rank: public — the signed-out /create landing page reads it; it is the same public
618
621
  // runbook the /new-project skill documents (no write, no secret).
619
622
  router.get('/provisioning/onboard-plan', (req, res) => {
620
623
  const mode = req.query.mode === 'adopt' ? 'adopt' : 'greenfield';
621
- res.json({ mode, steps: onboardPlan(mode) });
624
+ const domain = provisioning.normalizeHostname(req.query.domain) || null;
625
+ res.json({ mode, steps: onboardPlan(mode, { domain }) });
622
626
  });
623
627
 
624
628
  // GET /provisioning/tls-check?domain=<host> — the Caddy on-demand-TLS ALLOW-LIST
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.66",
3
+ "version": "1.19.68",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.66",
9
+ "version": "1.19.68",
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.19.66",
3
+ "version": "1.19.68",
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",
@@ -251,6 +251,14 @@ input[type=text].bad{border-color:var(--err);}
251
251
  }
252
252
  .steps b{font-size:15px;}
253
253
  .steps p{font-size:13.5px;font-weight:500;color:var(--ink-soft);line-height:1.55;margin-top:4px;}
254
+ /* live checklist states (task 1002694): a step the platform has verified done, and a
255
+ step that can't be performed yet (its prerequisite — usually the domain — is missing) */
256
+ .steps>li.sdone{border-color:#bbe7c8;background:linear-gradient(180deg,#f0fdf4 0%,#fcfffc 100%);}
257
+ .steps>li.sdone::before{content:"✓";background:var(--ok);color:#fff;}
258
+ .steps>li.slocked{opacity:.6;}
259
+ .steps details{margin-top:10px;}
260
+ .steps summary{cursor:pointer;font-weight:700;font-size:12.5px;color:var(--ink-faint);}
261
+ .steps details p{margin-top:8px;}
254
262
  .kv{
255
263
  display:flex;align-items:center;gap:8px;margin-top:9px;background:var(--bg);border:1.5px solid var(--line);
256
264
  border-radius:11px;padding:8px 8px 8px 12px;font-size:12.5px;
@@ -727,7 +735,10 @@ input[type=text].bad{border-color:var(--err);}
727
735
  } else if (clash) {
728
736
  hint.className = 'hint err'; hint.innerHTML = 'You already have a repo named <strong>' + esc(state.repo) + '</strong> — pick another name, or adopt it instead.';
729
737
  } else {
730
- hint.className = 'hint'; hint.innerHTML = 'We’ll create an empty repo at <code style="font-family:var(--font-mono)">github.com/' + esc(myLogin()) + '/' + esc(state.repo || 'name') + '</code>.';
738
+ /* Visibility stated up front (task 1002694 / idea 1000684): repos WE create are
739
+ public by design (github-repos.js — open-source, capture-resistant); the
740
+ private path is adopting a private repo the owner makes themselves. */
741
+ hint.className = 'hint'; hint.innerHTML = 'We’ll create an empty <strong>public</strong> repo at <code style="font-family:var(--font-mono)">github.com/' + esc(myLogin()) + '/' + esc(state.repo || 'name') + '</code> — projects here are open by design. Want private code? Create the repo yourself on GitHub and “adopt” it instead.';
731
742
  }
732
743
  }
733
744
  $('ghNewName').addEventListener('input', function () { state.repo = this.value.trim(); updateFreshPreview(); saveDraft(); });
@@ -983,53 +994,203 @@ input[type=text].bad{border-color:var(--err);}
983
994
  });
984
995
  });
985
996
 
986
- /* ── done panel: the canonical human steps (same sequence as the /new-project
987
- skill + `bongos onboard` + the hall wizard — goal 35; ADR 0121 branches
988
- only the repo leg on greenfield vs adopt) ── */
997
+ /* ── done panel: rendered FROM the canonical onboard plan (task 1002694, gap 2 /
998
+ idea 1000675). GET /provisioning/onboard-plan is the single source the
999
+ /new-project skill + `bongos onboard` read too, so the checklist can't
1000
+ drift. A per-key renderer adds this page's rich UI; an unknown human step
1001
+ falls back to the plan's own label, so a new canonical step surfaces here
1002
+ by default instead of silently vanishing. ── */
989
1003
  function copyBtn(text) {
990
1004
  return '<button type="button" class="copy" data-copy="' + esc(text) + '">Copy</button>';
991
1005
  }
992
- function renderDone(data) {
993
- var adopt = state.mode === 'adopt';
994
- var oa = state.domain ? oauthAppValues(state.domain) : null;
995
- $('doneLede').innerHTML = 'Provisioning is requested for <strong>' + esc(state.name || state.slug) + '</strong>' +
996
- (adopt ? ' — adopting your existing repo' : '') +
997
- '. The platform stands up the server, database, and address on its own — you can watch its live status right here.';
1006
+ var GH_ICON = '<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>';
1007
+
1008
+ var instanceId = null; // set when a create lands / a resumed watch starts
1009
+ var live = { instStatus: null, manifest: null, manifestMsg: '' };
1010
+ var planPromises = {}; // mode|domain → Promise<steps[]>
1011
+ var lastStepsHtml = '';
1012
+ var FALLBACK_HUMAN_KEYS = ['github-repo', 'oauth-app', 'first-sign-in', 'enter-hall'];
1013
+
1014
+ function fetchPlan() {
1015
+ var domain = state.domain || '';
1016
+ var key = state.mode + '|' + domain;
1017
+ if (!planPromises[key]) {
1018
+ planPromises[key] = fetch(API + '/provisioning/onboard-plan?mode=' + encodeURIComponent(state.mode) + (domain ? '&domain=' + encodeURIComponent(domain) : ''))
1019
+ .then(function (res) { if (!res.ok) throw new Error('plan'); return res.json(); })
1020
+ .then(function (d) { return (d && d.steps) || []; })
1021
+ .catch(function () {
1022
+ delete planPromises[key]; // transient — retried on the next render
1023
+ return FALLBACK_HUMAN_KEYS.map(function (k) { return { key: k, kind: 'human', label: '' }; });
1024
+ });
1025
+ }
1026
+ return planPromises[key];
1027
+ }
998
1028
 
999
- var steps = [];
1029
+ /* github-repo — adopt / auto-created / manual-create, each aware of whether the
1030
+ scaffold has landed (instance active ⇒ the build already pushed). The wizard
1031
+ files onboard_mode to the server, so for adopt the RUNNER clones + layers
1032
+ (task 1002562) — the owner no longer runs `bongos init --adopt` here. */
1033
+ function renderStepGithubRepo() {
1034
+ var landed = live.instStatus === 'active';
1000
1035
  var repoLabel = state.owner && state.repo ? '<strong>' + esc(state.owner + '/' + state.repo) + '</strong>' : 'your new repository';
1036
+ if (state.mode === 'adopt') {
1037
+ return landed
1038
+ ? '<li class="sdone" data-onboard-step="github-repo"><b>Your repo carries the platform</b><p>The build layered the platform onto ' + repoLabel + ' — existing files, history, and backlog kept. Pull to see it.</p></li>'
1039
+ : '<li data-onboard-step="github-repo"><b>Your existing repo</b><p>The build clones ' + repoLabel + ' and layers the platform onto its existing history — nothing is created or emptied, and your backlog is imported. Nothing for you to do here.</p></li>';
1040
+ }
1041
+ if (state.repoAutoCreated) {
1042
+ return landed
1043
+ ? '<li class="sdone" data-onboard-step="github-repo"><b>Repo ready — scaffold landed</b><p>We created ' + repoLabel + ' (public, by design) and the build pushed your project’s starting files to it.</p></li>'
1044
+ : '<li data-onboard-step="github-repo"><b>Your empty repo is ready</b><p>We created ' + repoLabel + ' on GitHub for you — public (projects here are open by design), empty, and waiting. The scaffold lands there when the build runs; nothing for you to do here.</p></li>';
1045
+ }
1001
1046
  var ghNewUrl = githubNewRepoUrl(state.owner, state.repo);
1002
- var ghIcon = '<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>';
1003
- steps.push(adopt
1004
- ? '<li data-onboard-step="github-repo"><b>Layer onto your existing repo</b><p>In your repo run <code style="font-family:var(--font-mono)">bongos init --adopt</code>. It shows a conflict pre-flight and writes <em>nothing</em> until you accept — your files and history stay yours, and your existing backlog is imported. Commit and push.</p></li>'
1005
- : (state.repoAutoCreated
1006
- ? '<li data-onboard-step="github-repo"><b>Your empty repo is ready</b><p>We created ' + repoLabel + ' on GitHub for you — empty and waiting. The scaffold lands there when the build runs; nothing for you to do here.</p></li>'
1007
- : '<li data-onboard-step="github-repo"><b>Create the empty GitHub repo</b><p>Do this first — the build needs the repo to exist. Click below (GitHub opens with the name already filled in), press <strong>Create repository</strong>, and leave it completely empty. Seeing “repository not found” above? That just means the repo isn’t created yet — make it here.</p>' +
1008
- '<a class="ghbtn" href="' + esc(ghNewUrl) + '" target="_blank" rel="noopener">' + ghIcon + 'Create ' + repoLabel + ' on GitHub</a></li>'));
1047
+ return '<li' + (landed ? ' class="sdone"' : '') + ' data-onboard-step="github-repo"><b>Create the empty GitHub repo</b><p>Do this first — the build needs the repo to exist. Click below (GitHub opens with the name already filled in), press <strong>Create repository</strong>, and leave it completely empty. Seeing “repository not found” above? That just means the repo isn’t created yet — make it here.</p>' +
1048
+ '<a class="ghbtn" href="' + esc(ghNewUrl) + '" target="_blank" rel="noopener">' + GH_ICON + 'Create ' + repoLabel + ' on GitHub</a></li>';
1049
+ }
1050
+
1051
+ /* The manual OAuth-app path stays as a documented fallback (ADR 0133), but the
1052
+ control-plane placement is named as the operator's, never handed to the owner
1053
+ as a command to run (gap 4). */
1054
+ function manualOauthDetails() {
1055
+ var oa = oauthAppValues(state.domain);
1056
+ return '<details><summary>Prefer to create an OAuth app by hand?</summary>' +
1057
+ '<p>GitHub → <em>your account’s</em> Settings → Developer settings → OAuth Apps → New OAuth App. (Developer settings live under your account, not the repo.)</p>' +
1058
+ '<div class="kv"><span class="kk">Homepage URL</span><code>' + esc(oa.homepageUrl) + '</code>' + copyBtn(oa.homepageUrl) + '</div>' +
1059
+ '<div class="kv"><span class="kk">Callback URL</span><code>' + esc(oa.callbackUrl) + '</code>' + copyBtn(oa.callbackUrl) + '</div>' +
1060
+ '<p>The generated credentials are placed on the platform’s control plane by an operator — that part isn’t yours to run (the one-click path above does it all automatically). It’s done when <code style="font-family:var(--font-mono)">' + esc(oa.homepageUrl) + '/healthz</code> reports <code style="font-family:var(--font-mono)">auth_configured: true</code>.</p></details>';
1061
+ }
1062
+
1063
+ /* oauth-app — the ADR 0133 one-click GitHub-App manifest flow is the primary path
1064
+ (gap 3 / idea 1000676); locked until a domain exists (the App needs the real
1065
+ callback host — the server refuses domainless with domain_required). */
1066
+ function renderStepOauthApp() {
1009
1067
  if (!state.domain) {
1010
- steps.push('<li><b>Add a domain you own</b><p>Sign-in on your new instance needs a real domain. When you have one, attach it from <a href="/projects">your projects page</a> — the address gets wired in minutes.</p></li>');
1068
+ return '<li class="slocked" data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>Unlocks once your project has an address — sign-in needs a real domain. Attach one any time from <a href="/projects">your projects page</a>.</p></li>';
1069
+ }
1070
+ if (live.manifest === 'done') {
1071
+ return '<li class="sdone" data-onboard-step="oauth-app"><b>GitHub sign-in is wired</b><p>Credentials placed and verified — sign-in on <code style="font-family:var(--font-mono)">' + esc(state.domain) + '</code> is live.</p></li>';
1072
+ }
1073
+ if (live.manifest === 'error' || live.manifest === 'expired') {
1074
+ return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>' +
1075
+ (live.manifest === 'expired' ? 'That setup link expired (they last 30 minutes) — mint a fresh one.' : 'GitHub setup hit a snag' + (live.manifestMsg ? ': ' + esc(live.manifestMsg) : '') + '. Try again.') + '</p>' +
1076
+ '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
1077
+ }
1078
+ if (live.manifest) { // pending / code_received — GitHub or the runner is mid-flight
1079
+ 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></li>';
1011
1080
  }
1012
- if (oa) {
1013
- steps.push('<li data-onboard-step="oauth-app"><b>Create the GitHub OAuth app</b><p>GitHub → <em>your account’s</em> Settings → Developer settings → OAuth Apps → New OAuth App. (Developer settings live under your account, not the repo.)</p>' +
1014
- '<div class="kv"><span class="kk">Homepage URL</span><code>' + esc(oa.homepageUrl) + '</code>' + copyBtn(oa.homepageUrl) + '</div>' +
1015
- '<div class="kv"><span class="kk">Callback URL</span><code>' + esc(oa.callbackUrl) + '</code>' + copyBtn(oa.callbackUrl) + '</div></li>');
1016
- var cmd = 'bongos oauth-secret place ' + state.slug + ' --client-id <id> --client-secret <secret> --apply';
1017
- steps.push('<li><b>Place the secret — verified</b><p>Never hand-edit the env file. On the control plane run the placement command; it checks the value is real and confirms sign-in works.</p>' +
1018
- '<div class="kv"><code>' + esc(cmd) + '</code>' + copyBtn(cmd) + '</div></li>');
1019
- steps.push('<li data-onboard-step="first-sign-in"><b>First sign-in</b><p>Open <code style="font-family:var(--font-mono)">' + esc(oa.homepageUrl) + '</code> and sign in with GitHub once. That seats you as the owner — the founding Archon.</p></li>');
1020
- steps.push('<li><b>Verify</b><p><code style="font-family:var(--font-mono)">' + esc(oa.homepageUrl) + '/healthz</code> should report <code style="font-family:var(--font-mono)">auth_configured: true</code>. Then the hall is yours.</p></li>');
1081
+ return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in — one click</b><p>Approve a pre-filled GitHub App and the platform wires sign-in by itself: no keys to copy, and the secret never leaves the server side.</p>' +
1082
+ '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
1083
+ }
1084
+
1085
+ function renderStepFirstSignIn() {
1086
+ if (!state.domain) {
1087
+ return '<li class="slocked" data-onboard-step="first-sign-in"><b>First sign-in</b><p>Unlocks with sign-in above — you’ll open your project’s address, sign in with GitHub once, and be seated as the owner (the founding Archon).</p></li>';
1021
1088
  }
1022
- $('humanSteps').innerHTML = steps.join('');
1023
- $('humanSteps').addEventListener('click', function (ev) {
1024
- var t = ev.target.closest('.copy');
1025
- if (!t) return;
1026
- navigator.clipboard.writeText(t.getAttribute('data-copy')).then(function () {
1027
- t.classList.add('did'); t.textContent = 'Copied';
1028
- setTimeout(function () { t.classList.remove('did'); t.textContent = 'Copy'; }, 1600);
1089
+ return '<li data-onboard-step="first-sign-in"><b>First sign-in</b><p>Open <a href="https://' + esc(state.domain) + '" target="_blank" rel="noopener"><code style="font-family:var(--font-mono)">' + esc(state.domain) + '</code></a> and sign in with GitHub once. That seats you as the owner — the founding Archon.</p></li>';
1090
+ }
1091
+
1092
+ function renderStepEnterHall() {
1093
+ return state.domain
1094
+ ? '<li data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>Your project’s home — open <a href="https://' + esc(state.domain) + '/builders" target="_blank" rel="noopener"><code style="font-family:var(--font-mono)">' + esc(state.domain) + '/builders</code></a> and start building.</p></li>'
1095
+ : '<li data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>No address yet — run <code style="font-family:var(--font-mono)">bongos dev</code> in your repo checkout to start the local hall on your machine.</p></li>';
1096
+ }
1097
+
1098
+ var STEP_RENDERERS = {
1099
+ 'github-repo': renderStepGithubRepo,
1100
+ 'oauth-app': renderStepOauthApp,
1101
+ 'first-sign-in': renderStepFirstSignIn,
1102
+ 'enter-hall': renderStepEnterHall
1103
+ };
1104
+
1105
+ function renderSteps() {
1106
+ fetchPlan().then(function (steps) {
1107
+ var html = steps.filter(function (s) { return s && s.kind === 'human'; }).map(function (s) {
1108
+ var r = STEP_RENDERERS[s.key];
1109
+ return r ? r(s) : '<li data-onboard-step="' + esc(s.key) + '"><b>' + esc(s.label || s.key) + '</b></li>';
1110
+ }).join('');
1111
+ if (html === lastStepsHtml) return; // change-only re-render — polls never clobber open UI
1112
+ lastStepsHtml = html;
1113
+ $('humanSteps').innerHTML = html;
1114
+ });
1115
+ }
1116
+
1117
+ function renderDone() {
1118
+ var adopt = state.mode === 'adopt';
1119
+ $('doneLede').innerHTML = 'Provisioning is requested for <strong>' + esc(state.name || state.slug) + '</strong>' +
1120
+ (adopt ? ' — adopting your existing repo' : '') +
1121
+ '. The platform stands up the server, database, and address on its own — you can watch its live status right here.';
1122
+ renderSteps();
1123
+ }
1124
+
1125
+ /* One delegated listener — renderSteps swaps innerHTML, so handlers live on the list. */
1126
+ $('humanSteps').addEventListener('click', function (ev) {
1127
+ var mBtn = ev.target.closest('[data-action="manifest-start"]');
1128
+ if (mBtn) { startManifest(mBtn); return; }
1129
+ var t = ev.target.closest('.copy');
1130
+ if (!t) return;
1131
+ navigator.clipboard.writeText(t.getAttribute('data-copy')).then(function () {
1132
+ t.classList.add('did'); t.textContent = 'Copied';
1133
+ setTimeout(function () { t.classList.remove('did'); t.textContent = 'Copy'; }, 1600);
1134
+ });
1135
+ });
1136
+
1137
+ /* ── GitHub-App manifest flow (ADR 0133): mint a one-time state for OUR instance,
1138
+ form-POST the pre-filled manifest to GitHub in a new tab (opened inside the
1139
+ click gesture so popup blockers allow it; blocked → same-tab, the callback
1140
+ page carries the rest), then poll the public status until the control-plane
1141
+ runner has placed + verified the credentials. ── */
1142
+ var manifestPollTimer = null;
1143
+ function stopManifestPoll() { if (manifestPollTimer) { clearTimeout(manifestPollTimer); manifestPollTimer = null; } }
1144
+
1145
+ function startManifest(btn) {
1146
+ if (!instanceId) return;
1147
+ btn.disabled = true; btn.textContent = 'Preparing GitHub…';
1148
+ var tab = window.open('about:blank', 'ghAppSetup');
1149
+ fetch(API + '/provisioning/instances/' + encodeURIComponent(instanceId) + '/github-app', {
1150
+ method: 'POST', credentials: 'same-origin',
1151
+ headers: { 'Content-Type': 'application/json' }, body: '{}'
1152
+ }).then(function (res) {
1153
+ if (res.status === 401) { window.location.assign(API + '/auth/web/start?return=/create'); throw new Error('signing in'); }
1154
+ return res.json().catch(function () { return {}; }).then(function (d) {
1155
+ if (!res.ok) {
1156
+ var e = new Error((d.error && (d.error.message || d.error.code)) || ('HTTP ' + res.status));
1157
+ throw e;
1158
+ }
1159
+ return d;
1029
1160
  });
1161
+ }).then(function (d) {
1162
+ var form = document.createElement('form');
1163
+ form.method = 'POST'; form.action = d.post_url; form.target = tab ? 'ghAppSetup' : '_self';
1164
+ var inp = document.createElement('input');
1165
+ inp.type = 'hidden'; inp.name = 'manifest'; inp.value = JSON.stringify(d.manifest);
1166
+ form.appendChild(inp); document.body.appendChild(form);
1167
+ form.submit(); form.remove();
1168
+ live.manifest = 'pending'; live.manifestMsg = '';
1169
+ renderSteps();
1170
+ pollManifest(d.state);
1171
+ }).catch(function (err) {
1172
+ if (String(err && err.message) === 'signing in') return;
1173
+ if (tab) { try { tab.close(); } catch (e) {} }
1174
+ live.manifest = 'error'; live.manifestMsg = String((err && err.message) || 'could not start');
1175
+ renderSteps();
1030
1176
  });
1031
1177
  }
1032
1178
 
1179
+ function pollManifest(stateToken) {
1180
+ stopManifestPoll();
1181
+ function tick() {
1182
+ fetch(API + '/provisioning/github-app/status?state=' + encodeURIComponent(stateToken))
1183
+ .then(function (res) { if (!res.ok) throw new Error('soft'); return res.json(); })
1184
+ .then(function (d) {
1185
+ var next = d.ready ? 'done' : (d.status || 'pending');
1186
+ if (next !== live.manifest) { live.manifest = next; renderSteps(); }
1187
+ if (!d.terminal) manifestPollTimer = setTimeout(tick, 4000);
1188
+ })
1189
+ .catch(function () { manifestPollTimer = setTimeout(tick, 8000); });
1190
+ }
1191
+ tick();
1192
+ }
1193
+
1033
1194
  /* ── live build progress (task 2083): poll the instance until it's up.
1034
1195
  Read-only — GET /provisioning/instances/:id (own-scoped) every 5s. ── */
1035
1196
  var pollTimer = null;
@@ -1073,6 +1234,7 @@ input[type=text].bad{border-color:var(--err);}
1073
1234
  }
1074
1235
  function startProgress(id) {
1075
1236
  if (!id) return;
1237
+ instanceId = id; // the manifest flow mints against this instance
1076
1238
  stopPolling();
1077
1239
  var ticks = 0;
1078
1240
  var el = $('liveProgress');
@@ -1097,7 +1259,12 @@ input[type=text].bad{border-color:var(--err);}
1097
1259
  return res.json();
1098
1260
  })
1099
1261
  .then(function (data) {
1100
- var terminal = renderProgress(data.instance);
1262
+ var inst = data.instance;
1263
+ /* the checklist reflects live state (task 1002694): a status transition —
1264
+ e.g. the scaffold landing — re-renders the steps (change-only, so the
1265
+ 5s tick never clobbers an open dropdown or mid-flight button) */
1266
+ if (inst && inst.status !== live.instStatus) { live.instStatus = inst.status; renderSteps(); }
1267
+ var terminal = renderProgress(inst);
1101
1268
  if (!terminal) pollTimer = setTimeout(tick, 5000);
1102
1269
  else stopPolling();
1103
1270
  })
@@ -1121,6 +1288,10 @@ input[type=text].bad{border-color:var(--err);}
1121
1288
  again.type = 'button'; again.className = 'again'; again.textContent = 'Start another project';
1122
1289
  again.addEventListener('click', function () {
1123
1290
  stopPolling();
1291
+ stopManifestPoll();
1292
+ instanceId = null;
1293
+ live = { instStatus: null, manifest: null, manifestMsg: '' };
1294
+ lastStepsHtml = '';
1124
1295
  try { sessionStorage.removeItem(CREATED_KEY); sessionStorage.removeItem(DRAFT_KEY); } catch (e) {}
1125
1296
  state = { step: 1, name: '', mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '' };
1126
1297
  $('f-name').value = ''; $('f-owner').value = ''; $('f-repo').value = ''; $('f-slug').value = ''; $('f-domain').value = '';
@@ -199,6 +199,15 @@ async function run() {
199
199
 
200
200
  // Step 10 — brand
201
201
  log('\n⑨ Brand the instance look — fill theme/palette/fonts in config/branding.json (docs/branding-fill-prompt.md).');
202
+
203
+ // Step 11 — enter the builders hall (task 1002703): the point of all of the above.
204
+ // With a domain the hall lives on the instance; without one, `bongos dev` runs it locally.
205
+ if (a.domain) {
206
+ log(`\n⑩ Enter your builders hall — open https://${a.domain}/builders. That's your project's home.`);
207
+ } else {
208
+ log('\n⑩ Enter your builders hall — no domain yet, so run `bongos dev` in your repo checkout:');
209
+ log(' it stands the instance up locally and serves the hall on your machine.');
210
+ }
202
211
  log('\n✓ onboard walkthrough complete. Your standalone instance is live and owned.\n');
203
212
  } finally {
204
213
  rl.close();
package/src/module-api.js CHANGED
@@ -559,8 +559,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
559
559
  // landed since 1.19.64 with no explicit bump. run 31562193292. (task 1002620)
560
560
  // 1.19.66 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
561
561
  // landed since 1.19.65 with no explicit bump. run 31599966075. (task 1002620)
562
+ // 1.19.67 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
563
+ // landed since 1.19.66 with no explicit bump. run 31605591253. (task 1002620)
564
+ // 1.19.68 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
565
+ // landed since 1.19.67 with no explicit bump. run 31608183268. (task 1002620)
562
566
  // ---------------------------------------------------------------------------
563
- const CORE_VERSION = '1.19.66'; // CI auto-patch carrier (ADR 0161); see changelog above
567
+ const CORE_VERSION = '1.19.68'; // CI auto-patch carrier (ADR 0161); see changelog above
564
568
 
565
569
  // A namespaced logger so a module's log lines are attributable + consistent.
566
570
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
package/tests/onboard.mjs CHANGED
@@ -7,14 +7,15 @@ import { createRequire } from 'node:module';
7
7
  const require = createRequire(import.meta.url);
8
8
  const ob = require('../scripts/gds/onboard.js');
9
9
 
10
- test('onboardPlan is the canonical sequence: name+address first, brand last, four legs automated around the human steps', () => {
10
+ test('onboardPlan is the canonical sequence: name+address first, the builders hall last, four legs automated around the human steps', () => {
11
11
  const plan = ob.onboardPlan();
12
12
  const keys = plan.map((s) => s.key);
13
13
  assert.equal(keys[0], 'name-address');
14
- assert.equal(keys[keys.length - 1], 'brand');
14
+ assert.equal(keys[keys.length - 1], 'enter-hall');
15
+ assert.equal(keys[keys.length - 2], 'brand');
15
16
  // the irreducibly-human steps (must be UI-first prompts)
16
17
  const human = plan.filter((s) => s.kind === 'human').map((s) => s.key);
17
- assert.deepEqual(human, ['github-repo', 'oauth-app', 'first-sign-in']);
18
+ assert.deepEqual(human, ['github-repo', 'oauth-app', 'first-sign-in', 'enter-hall']);
18
19
  // the DNS preflight comes BEFORE provision (task 1980: before the address step)
19
20
  assert.ok(keys.indexOf('dns-preflight') < keys.indexOf('provision'), 'dns preflight precedes provision');
20
21
  // OAuth secret placement comes after the OAuth app + before first sign-in
@@ -52,6 +53,63 @@ test('onboardPlan lists seed-first-version as an explicit AUTO step right after
52
53
  assert.ok(ob.onboardPlan('adopt').some((s) => s.key === 'seed-version'), 'adopt gets it too');
53
54
  });
54
55
 
56
+ test('onboardPlan ends at the builders hall, terminal label branching on domain (task 1002703)', () => {
57
+ // The hall is the entire point of creating a project, yet the sequence used to stop
58
+ // at brand — owners only learned about `bongos dev` or their hall URL by being told
59
+ // (idea 1000693). One terminal step in the ONE source reaches every render surface.
60
+ for (const mode of ['greenfield', 'adopt']) {
61
+ const dflt = ob.onboardPlan(mode);
62
+ const last = dflt[dflt.length - 1];
63
+ assert.equal(last.key, 'enter-hall', `${mode}: the plan ends at the hall`);
64
+ assert.equal(last.kind, 'human', 'entering the hall is the owner\'s own action');
65
+ assert.match(last.label, /bongos dev/, 'no domain → the local-hall one-liner');
66
+
67
+ const withDomain = ob.onboardPlan(mode, { domain: 'mercury.example.com' });
68
+ const lastD = withDomain[withDomain.length - 1];
69
+ assert.equal(lastD.key, 'enter-hall');
70
+ assert.match(lastD.label, /https:\/\/mercury\.example\.com\/builders/, 'domain → the instance\'s hall address');
71
+ assert.doesNotMatch(lastD.label, /bongos dev/, 'with a domain the local fallback is noise');
72
+ // only the terminal label branches — the skeleton is identical
73
+ assert.deepEqual(withDomain.map((s) => s.key), dflt.map((s) => s.key));
74
+ }
75
+ // a blank / whitespace domain is "no domain"
76
+ const blank = ob.onboardPlan('greenfield', { domain: ' ' });
77
+ assert.match(blank[blank.length - 1].label, /bongos dev/);
78
+ });
79
+
80
+ test('every render surface carries the terminal hall step (task 1002703)', () => {
81
+ // The human-steps drift guard below pins the TAG; this pins the CONTENT — each
82
+ // surface must point at the hall (domain branch) and at `bongos dev` (local branch).
83
+ const { readFileSync } = require('node:fs');
84
+ const path = require('node:path');
85
+ const read = (rel) => readFileSync(path.join(import.meta.dirname, '..', rel), 'utf8');
86
+ const cli = read('scripts/gds/onboard.js');
87
+ assert.match(cli, /builders hall/i, 'the CLI walkthrough ends at the hall');
88
+ assert.match(cli, /bongos dev/, 'the CLI offers the local-hall one-liner when no domain');
89
+ const createHtml = read('public-landing/create.html');
90
+ const enterHall = createHtml.match(/data-onboard-step="enter-hall"[^]*?<\/li>/g) || [];
91
+ assert.ok(enterHall.length >= 1, '/create tags the enter-hall step');
92
+ assert.match(enterHall.join(''), /\/builders/, '/create points at the hall address');
93
+ assert.match(enterHall.join(''), /bongos dev/, '/create offers the local hall when no domain');
94
+ });
95
+
96
+ test('the /create done panel is plan-driven, manifest-driven, and owner-honest (task 1002694)', () => {
97
+ // Gaps 2/3/4/14 of the project-creation audit (docs/audits/project-creation-gap-list.md).
98
+ const { readFileSync } = require('node:fs');
99
+ const path = require('node:path');
100
+ const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/create.html'), 'utf8');
101
+ // gap 2 (idea 1000675): the checklist renders FROM the canonical plan endpoint
102
+ assert.match(src, /provisioning\/onboard-plan/, 'the done panel fetches the canonical plan');
103
+ // gap 3 (idea 1000676): the ADR 0133 one-click manifest flow has a UI caller again
104
+ assert.match(src, /\/github-app/, 'the done panel drives the GitHub-App manifest flow');
105
+ assert.match(src, /github-app\/status/, 'and polls its progress to reflect live state');
106
+ // gap 4: the operator-only control-plane command never reaches a new owner
107
+ assert.doesNotMatch(src, /oauth-secret place/, 'owners are never handed the control-plane placement command');
108
+ // gap 14 (idea 1000684): repo visibility is stated, with the private path named
109
+ assert.match(src, /public/, 'visibility is stated');
110
+ assert.match(src, /adopt/i, 'the private path (adopt your own private repo) is named');
111
+ });
112
+
55
113
  test('oauthAppInstructions pins the apex callback + homepage and points at ACCOUNT Developer settings', () => {
56
114
  const oa = ob.oauthAppInstructions('mercury.example.com');
57
115
  assert.equal(oa.homepageUrl, 'https://mercury.example.com');
@@ -113,7 +171,10 @@ test('the web form (/create) declares EXACTLY the canonical human steps — drif
113
171
  };
114
172
  for (const [name, rel] of Object.entries(surfaces)) {
115
173
  const src = readFileSync(path.join(import.meta.dirname, '..', rel), 'utf8');
116
- const found = [...src.matchAll(/data-onboard-step="([^"]+)"/g)].map((m) => m[1]);
174
+ // keys are kebab-case literals; the charset excludes the page's DYNAMIC fallback
175
+ // tag (data-onboard-step="' + esc(s.key) + '"), which renders unknown plan steps
176
+ // at runtime and has no literal key to pin here (task 1002694).
177
+ const found = [...src.matchAll(/data-onboard-step="([a-z0-9-]+)"/g)].map((m) => m[1]);
117
178
  const uniq = [...new Set(found)].sort();
118
179
  assert.deepEqual(uniq, humanKeys, `${name} (${rel}) must tag exactly the canonical human steps [${humanKeys}], found [${uniq}]`);
119
180
  }
@@ -162,6 +162,32 @@ await ta('POST /instances: an unknown onboard_mode is REFUSED (never silently co
162
162
  assert.deepEqual(res.body.error.valid, ['greenfield', 'adopt']);
163
163
  });
164
164
 
165
+ function get(path) {
166
+ return new Promise((resolve, reject) => {
167
+ http.get(`${base}${path}`, (res) => {
168
+ let d = '';
169
+ res.on('data', (c) => { d += c; });
170
+ res.on('end', () => { let j = null; try { j = JSON.parse(d); } catch { /* non-JSON */ } resolve({ status: res.statusCode, body: j, raw: d }); });
171
+ }).on('error', reject);
172
+ });
173
+ }
174
+
175
+ await ta('GET /onboard-plan: ?domain= relabels the terminal enter-hall step; garbage is ignored, never echoed (task 1002703)', async () => {
176
+ const bare = await get('/provisioning/onboard-plan');
177
+ assert.equal(bare.status, 200);
178
+ const bareLast = bare.body.steps[bare.body.steps.length - 1];
179
+ assert.equal(bareLast.key, 'enter-hall');
180
+ assert.match(bareLast.label, /bongos dev/, 'no domain → the local-hall one-liner');
181
+ const dom = await get('/provisioning/onboard-plan?domain=mercury.example.com');
182
+ const domLast = dom.body.steps[dom.body.steps.length - 1];
183
+ assert.match(domLast.label, /https:\/\/mercury\.example\.com\/builders/, 'domain → that instance\'s hall');
184
+ // clients render these labels — a non-hostname must be dropped, not reflected back
185
+ const bad = await get('/provisioning/onboard-plan?domain=' + encodeURIComponent('<script>alert(1)</script>'));
186
+ const badLast = bad.body.steps[bad.body.steps.length - 1];
187
+ assert.match(badLast.label, /bongos dev/);
188
+ assert.ok(!badLast.label.includes('<script>'), 'unvalidated input never reaches the label');
189
+ });
190
+
165
191
  await ta('POST /instances: adopt WITHOUT target_ref is refused — there is no repo to layer onto', async () => {
166
192
  // Without target_ref the runner would fall back to greenfield behavior on a row labelled
167
193
  // adopt. Refuse at the door rather than store a lie.