@bongos/core 1.19.576 → 1.19.578

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,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.576",
6
- "core_contract": "1.19.576",
7
- "source_commit": "a97463936d818b3a6be556837447748b3a2abaa1",
5
+ "core_version": "1.19.578",
6
+ "core_contract": "1.19.578",
7
+ "source_commit": "a3f42dff6ab98712306ee86eaedf8191cafa3340",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-07T14:24:36.180Z",
9
+ "built_at": "2026-09-07T17:15:54.070Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 450,
12
+ "docs_redacted": 451,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2044,
14
+ "functional_verbatim": 2047,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2518,
20
- "tree_sha256": "9b56f557f5e1d8d63c314cb2990df89c75d7230c3f16bdd41416396d95e2b6bd",
19
+ "file_count": 2522,
20
+ "tree_sha256": "96be19d76442ed1b3e663525652a70c6ef60434e80d4ad907df8e294be5f38fe",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -1804,10 +1804,15 @@
1804
1804
  "mode": "0000644",
1805
1805
  "sha256": "5ebfaa564a125f2c5bb0b391b8ff01914f147c15c9ef3f4caa91bafa8bd2e8ed"
1806
1806
  },
1807
+ {
1808
+ "path": "docs/adr/0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md",
1809
+ "mode": "0000644",
1810
+ "sha256": "28f3644d3d9dcd2985b7e3f42cf2135af86203eee35b6e509d1ff98f630f1228"
1811
+ },
1807
1812
  {
1808
1813
  "path": "docs/adr/README.md",
1809
1814
  "mode": "0000644",
1810
- "sha256": "e9b47055a52c284035c201fae3f172215c746caabf938922821f4efcda2e8980"
1815
+ "sha256": "30bb35e65ebd2dc8c5eb0aad7e30d5875d0c764265f398547abd545b62ee7d7c"
1811
1816
  },
1812
1817
  {
1813
1818
  "path": "docs/api-reference.md",
@@ -2682,7 +2687,7 @@
2682
2687
  {
2683
2688
  "path": "docs/file-map.md",
2684
2689
  "mode": "0000644",
2685
- "sha256": "dd40cd29025789ec5a9447aa44ec6c7d3bee6aaae183c65713a6a3ca8ed8275b"
2690
+ "sha256": "f43e8946ecb5c96dc7af0bcee49010245df577eee62381701d2aef1c67455662"
2686
2691
  },
2687
2692
  {
2688
2693
  "path": "docs/handoff-template.md",
@@ -2692,7 +2697,7 @@
2692
2697
  {
2693
2698
  "path": "docs/module-api-changelog.md",
2694
2699
  "mode": "0000644",
2695
- "sha256": "8f122cc3f3ca35dcc6569b98bad34967f2a4bbf9ebcbc0e645ca2b1016245fbc"
2700
+ "sha256": "072128c45a7036c0d01bc029a1267bf34ff7bd175226e83d691b8001e4a8eefa"
2696
2701
  },
2697
2702
  {
2698
2703
  "path": "docs/modules-contract.md",
@@ -6807,7 +6812,7 @@
6807
6812
  {
6808
6813
  "path": "modules/public-landing/public/projects.html",
6809
6814
  "mode": "0000644",
6810
- "sha256": "7aa74c059670bd351bf8a4295e408aef4a908e3095711aee93f75408ccd837d5"
6815
+ "sha256": "22201cd63fde334156b40954707d836f8c65a8380a3f346fe7c77abb03ed3b51"
6811
6816
  },
6812
6817
  {
6813
6818
  "path": "modules/public-landing/public/projects.probes.json",
@@ -7552,12 +7557,12 @@
7552
7557
  {
7553
7558
  "path": "package-lock.json",
7554
7559
  "mode": "0000644",
7555
- "sha256": "8a58247d11bee28e30582d0860bd54cac15c8f3cb30753111063711a9369c390"
7560
+ "sha256": "e5249bc21a0842ce0e47203e1b7052aa146161702db57b1c50740d409a7347b2"
7556
7561
  },
7557
7562
  {
7558
7563
  "path": "package.json",
7559
7564
  "mode": "0000644",
7560
- "sha256": "e2f634e2e1deae3517a6b545c91a2b4818c51b04cb56daa4f1528b822cc1d14e"
7565
+ "sha256": "d328d1b9a42a9003642815e8a17cc14f648d53a996c2c6db92a8d1250c570ea8"
7561
7566
  },
7562
7567
  {
7563
7568
  "path": "public-docs/index.html",
@@ -7727,7 +7732,7 @@
7727
7732
  {
7728
7733
  "path": "scripts/gds/box-connect-lib.js",
7729
7734
  "mode": "0000644",
7730
- "sha256": "73172141b376910a6b6bc5a326e6bebae1fb8d0a80441e18c9f5f7acf689b67d"
7735
+ "sha256": "56c6f3eae23d427d9c74763521739bae5b704bcd02ad234e43a5434645657519"
7731
7736
  },
7732
7737
  {
7733
7738
  "path": "scripts/gds/box-infra.js",
@@ -7754,6 +7759,11 @@
7754
7759
  "mode": "0000644",
7755
7760
  "sha256": "6a696f9291da2d071d8a8aa0f92bf0be08445569a73196f9204f696bf6cb6cb5"
7756
7761
  },
7762
+ {
7763
+ "path": "scripts/gds/build-cli-package.js",
7764
+ "mode": "0000644",
7765
+ "sha256": "4b2428615e3c81c14a3b3144eca35fcb2c4523c4debef0f7d4596e5934857a6b"
7766
+ },
7757
7767
  {
7758
7768
  "path": "scripts/gds/bump-version.js",
7759
7769
  "mode": "0000644",
@@ -9257,7 +9267,7 @@
9257
9267
  {
9258
9268
  "path": "src/module-api.js",
9259
9269
  "mode": "0000644",
9260
- "sha256": "fb020c72dfb07b789b7e281ba766fe8f0f9c4fe67c359c8b5c9928c8e86b85da"
9270
+ "sha256": "71207830c631f2604b55d9e2e6a2db08b0d129499249c4db009c00e871fc2fd7"
9261
9271
  },
9262
9272
  {
9263
9273
  "path": "src/module-loader/catalog.js",
@@ -9839,6 +9849,11 @@
9839
9849
  "mode": "0000644",
9840
9850
  "sha256": "d0ea5b273c24527190f6ed01555ddb6b022a6ea94f3efcc7bdbd17f834c7d2ad"
9841
9851
  },
9852
+ {
9853
+ "path": "tests/cli_package.mjs",
9854
+ "mode": "0000644",
9855
+ "sha256": "a57516ed9e904cf1c86fe99688415f834dcae1a19f01b22e986f199fa5473258"
9856
+ },
9842
9857
  {
9843
9858
  "path": "tests/cli_token_reissue.mjs",
9844
9859
  "mode": "0000644",
@@ -11164,6 +11179,11 @@
11164
11179
  "mode": "0000644",
11165
11180
  "sha256": "9a1813d6036886ea07d1563e71a466e66c28554b75b22605464c795fd9fa2ebb"
11166
11181
  },
11182
+ {
11183
+ "path": "tests/manage_manifest_shared_read.mjs",
11184
+ "mode": "0000644",
11185
+ "sha256": "419234978bb7688f83ec14bb2db7fb4be1eec99205595795c705befe5620b89c"
11186
+ },
11167
11187
  {
11168
11188
  "path": "tests/manage_settings_shared_read.mjs",
11169
11189
  "mode": "0000644",
@@ -11627,7 +11647,7 @@
11627
11647
  {
11628
11648
  "path": "tests/projects_hub.mjs",
11629
11649
  "mode": "0000644",
11630
- "sha256": "f93044429430c9fff4fe6860bae87ff8f67bc49a485391e659422e63f209e08c"
11650
+ "sha256": "ec536c7bcfe2c9ea29d0df8a0dcbbb5c2764b4f60e0d14218aa0a10bf6afb469"
11631
11651
  },
11632
11652
  {
11633
11653
  "path": "tests/projects_hub_module_picker.mjs",
@@ -0,0 +1,140 @@
1
+ # ADR 0258 — The public CLI is a generated client-only package, and its file list is proven by running it
2
+
3
+ - **Status:** accepted
4
+ - **Date:** 2026-09-07
5
+ - **Task:** [task 1003679](https://cloudbongos.com/builders#/task/1003679) (BONGOS-V1, goal 1000054 — *A newcomer can build without the UI*)
6
+ - **Deciders:** Claude, under the Archon's standing scope
7
+ - **Related:** [ADR 0083](<redacted>.md) (a module may import only `src/module-api.js` — the reason a static closure cannot answer this question) · [ADR 0099](<redacted>.md) (the lagged, redacted public mirror — still dormant) · [ADR 0108](<redacted>.md) (the core ships as a private versioned dependency) · [ADR 0118](<redacted>.md) (the generated client this package vendors) · [ADR 0161](<redacted>.md) (why the core's version moves many times a day)
8
+
9
+ ---
10
+
11
+ ## Context
12
+
13
+ The core ships as `@bongos/core`, which is **private**. That is deliberate (ADR 0108), and it
14
+ has an unintended consequence at the front door: a newcomer with no credential can install
15
+ nothing. There is no terminal path into an instance at all, so the web hall is the only way in.
16
+
17
+ The owner put it plainly: *"how am I supposed to easily take on tasks as a new builder? I should
18
+ be able to do everything without the UI."*
19
+
20
+ An open task, 1002025, proposed the obvious fix — publish the core to npm. That is the wrong
21
+ instrument on two counts. It publishes the **server**: routes, auth middleware, pool, migrations,
22
+ provisioning. And it bypasses ADR 0099's redaction pipeline, which is the mechanism that is
23
+ supposed to decide what leaves this repo and does not run until ~2026-11. The premise was also
24
+ stale (it named `@cloudbongos/core`; the package is `@bongos/core`).
25
+
26
+ Two refactors landed the night before this decision and are what make a narrower answer possible:
27
+ [task 1003676](https://cloudbongos.com/builders#/task/1003676) cut the one edge that dragged the
28
+ server into the CLI, and [task 1003677](https://cloudbongos.com/builders#/task/1003677) made
29
+ `src/module-api.js` resolve its kernel capabilities on read rather than on import.
30
+
31
+ ## Decision
32
+
33
+ **Generate a separate, public, client-only package — `@cloudbongos/cli` — from the core, and do
34
+ not publish the core.** `scripts/gds/build-cli-package.js` emits it; `tests/cli_package.mjs`
35
+ proves it.
36
+
37
+ Four parts of this are load-bearing.
38
+
39
+ ### 1. The name is scoped because the org already exists
40
+
41
+ The owner holds the npm org `cloudbongos`. On npm, org names and unscoped package names share one
42
+ namespace, so the bare name `cloudbongos` is **not publishable precisely because that org
43
+ exists** — npm reports it as invalid, which reads like the name is taken when in fact the owner
44
+ owns it. A public scoped package is free and `npx @cloudbongos/cli` is not a worse experience
45
+ than `npx cloudbongos`. (Registry checked 2026-09-07: `cloudbongos`, `@cloudbongos/cli` and
46
+ `@cloudbongos/core` all 404, search total 0.)
47
+
48
+ Related and worth writing down because it wasted the owner's time: **a package name cannot be
49
+ created or reserved on the npm website.** The name comes into existence on first `npm publish`.
50
+ Every field on the org settings pages operates on packages that already exist, which is why
51
+ "Add Existing Package" answered `Forbidden` for a package nobody had published.
52
+
53
+ ### 2. The file list is DECLARED, and proven by behaviour — not computed
54
+
55
+ The tempting approach is to derive the package from a static require-closure of the CLI entry
56
+ points. **That instrument does not work here, and cannot be made to.** `src/module-api.js` is the
57
+ published module doorway (ADR 0083): a module may import *only* it, so any script that touches a
58
+ module file statically reaches the doorway, and the doorway by design *names* every kernel
59
+ capability. A static walk therefore sees the whole server — 33 `src/` files from
60
+ `scripts/gds/start.js` alone — even though every one of those names is behind a lazy getter that
61
+ never resolves. Measured at runtime the same set is **one** `src/bongos/` file
62
+ (`api-prefix.js`, which imports nothing).
63
+
64
+ This is the same trap as task 1003677's original done-when ("zero `src/bongos` in the
65
+ require-closure"), which was unsatisfiable for exactly this reason: a getter still *contains* the
66
+ require text.
67
+
68
+ So `FILES` in the build script is an explicit, grouped, auditable list, and the test packs the
69
+ tarball, installs it into an empty directory **with no repo present**, and runs every advertised
70
+ verb. A verb may fail for want of a session or a network; it may not fail because a file is
71
+ missing. That test is the only thing that can prove the list complete, and it earned its keep
72
+ immediately — the first build shipped without `clients/bongos-client/index.mjs`, which
73
+ `cli-lib.js` reaches by a dynamic `import()` that no `require()`-based reasoning would ever see.
74
+ The build now refuses any relative `import()` target the manifest omits, and the generated
75
+ `package.json` `files` array is derived from the manifest rather than hand-listed (the hand-listed
76
+ one had already dropped `clients/`, producing a tarball that installed cleanly and died on first
77
+ use).
78
+
79
+ ### 3. `claim` and `ship` are absent, and the CLI says why
80
+
81
+ Writing code needs a real checkout and a worktree, so `claim`, `ship`, `dev`, `serve`, `module`,
82
+ `upgrade`, `onboard`, `doctor`, `exec` and `package-core` are not in the public CLI. A newcomer
83
+ who types one gets the reason and the next step — `bongos shell` opens a terminal on a cloud dev
84
+ box that already has the full CLI — rather than `unknown command`. A bare unknown-command error
85
+ teaches nothing, which is the precise failure this whole task exists to fix, so it would be
86
+ perverse to reintroduce it at the boundary.
87
+
88
+ The journey the package supports end to end is: `bongos login <instance>` → `bongos start` →
89
+ `bongos shell`. Steps one and two are what had no terminal path at all.
90
+
91
+ `onboard` is excluded for a second reason: it drags ten provisioning files that talk to
92
+ DigitalOcean and GitHub. It is owner tooling, and it is the widest redaction surface in the
93
+ candidate set.
94
+
95
+ ### 4. A redaction gate runs on every build, and it fails closed
96
+
97
+ The package is public, so the build scans the emitted tree and **refuses to emit** on a hit:
98
+ non-loopback IPv4 (RFC 5737 documentation ranges and link-local exempted — those can never name
99
+ real infrastructure, so they are the correct thing to write in an example), GitHub/npm token
100
+ shapes, private-key blocks, AWS keys, plus every domain and owner login it can read out of the
101
+ instance's own `config/branding.json`.
102
+
103
+ The needles come from host config rather than a list in the core, so the core carries no instance
104
+ identity (the ADR 0062 §7 split). A neutral core has no `config/branding.json`, so that half is
105
+ inert there — which is correct: the gate exists to stop someone building this package from an
106
+ *instance* checkout. `cloudbongos.com` is allowlisted as public by design, the way
107
+ `registry.npmjs.org` is baked into npm.
108
+
109
+ The generated `package.json` carries **no `repository` field**: the core repo is private, so a
110
+ repository URL would 404 for every user *and* put the owner's login into a public artifact for no
111
+ benefit. It gains one when ADR 0099's mirror lands.
112
+
113
+ ## Consequences
114
+
115
+ - 31 files, ~450 KB, 13 verbs, **one** runtime dependency (`undici`). Small enough to audit by
116
+ reading, which was the point.
117
+ - The package's version is independent of the core's. The core moves many times a day under
118
+ ADR 0161; a public CLI's contract should not.
119
+ - The generated tree is gitignored (`dist/`). It is rebuilt, never committed.
120
+ - **Publishing is the owner's action, not a builder's.** It is the owner's npm account, and a
121
+ public package name cannot be un-taken. The build stops at a packed tarball and hands over one
122
+ `npm publish --access public`.
123
+ - `@bongos/client` stays private and is **vendored** as a single file rather than declared as a
124
+ dependency — a public package that depended on a private one would be uninstallable for
125
+ everyone.
126
+ - Task 1002025 is superseded and should be closed against this ADR rather than done.
127
+
128
+ ## Rejected
129
+
130
+ - **Publishing the core** (task 1002025 as written) — ships the server and bypasses ADR 0099.
131
+ - **Deriving the package from a static require-closure** — structurally impossible past the
132
+ module doorway; see §2.
133
+ - **Waiting for the ADR 0099 mirror** — it is dormant until ~2026-11 and the front door is broken
134
+ now. The two are independent: this package is a build product, not a source release.
135
+ - **An unscoped `cloudbongos` package** — unpublishable while the owner's org of that name exists.
136
+ - **Depending on `@bongos/client`** — private, so the public package would not install.
137
+ - **Including `claim`/`ship` in a degraded form** — a half-working claim that cannot materialize
138
+ into a checkout is worse than one that explains where to go.
139
+ - **Shipping all of `src/`** to make lazy getters safe — reintroduces the server surface and the
140
+ redaction problem the narrow list exists to avoid.
@@ -349,3 +349,4 @@ This keeps the decision history honest and traceable.
349
349
 
350
350
  > ⚠️ **Numbering collisions are now machine-enforced, not narrated.** Twenty numbers are shared by two ADRs each — assigned in parallel sessions before anything checked. The files keep their filenames (renumbering would break every existing citation, and a number once assigned is never reused), and this table disambiguates each pair as `NNNN-a` / `NNNN-b`. The authoritative list is `LEGACY_DUPLICATE_ADRS` in [`scripts/gds/adr-namespace.js`](../../scripts/gds/adr-namespace.js), frozen by exact filename: a **new** collision — or a third file joining a legacy number, or a rename of either half — hard-fails the `unit` CI gate, as does an index row that points at no file or an ADR with no row. The list may only shrink. This paragraph used to narrate the collisions one by one and had fallen eight behind reality; see [ADR 0195](<redacted>.md) for why the check exists and why the pairs are grandfathered rather than renumbered.
351
351
  | 0257 | [**Auth resolves before the hall mounts anything, and a widget's boot read may never navigate** ([task 1003673](https://cloudbongos.com/builders#/task/1003673) · goal 1000063 — *The front door*). An invite-only instance could not admit its FIRST builder, and the symptom lied about where the fault was: an owner saw an empty Access-requests queue and no approve button, because **signing in does not file a request** — only the landing's *Request access* form does, and that form was unreachable. A signed-out visitor to `/builders` was bounced to GitHub, refused as a first-timer ("request access first"), and pointed back at `/builders` to be bounced again. Reproduced against `main`, not just an old pin. `builders.js` was already careful — `/me` is read `softAuth` and a 401 there renders the landing — but `DOMContentLoaded` called `mountHallWidgets()` FIRST, synchronously, and `goals.js`'s mount-time read of `/goals` goes through a `getJSON` with no `softAuth`, which answers 401 by NAVIGATING. The widget's read raced `renderLanding()` and won. **Decision: the front door settles which page this is before anything else runs** — `boot()` reads `/me` (soft), then either renders the landing and stops (nothing mounts, nothing else fetches) or mounts the hall, handing the resolved payload to `loadAll()` so the page still asks once. Widgets are member surfaces (`renderLanding` hid them all after the fact anyway), so not mounting them while signed out removes the CLASS rather than the one instance of it that was found; **a widget's own boot read may never navigate** is the belt beside it (`goals.js` reads soft and renders nothing without a session). A 401 during boot is not an instruction to go and sign in — only a user action is. `tests/hall_landing_boot.mjs` executes the real `builders.js` in a DOM stub and was confirmed to FAIL against the pre-fix file. Rejected: patching `goals.js` alone (it was merely first); making the 401 redirect soft everywhere (an expired session mid-visit SHOULD be sent to sign in — the line is boot vs user action); open enrollment as the cure (removes the gate [ADR 0050](<redacted>.md) chose deliberately instead of repairing the door).](<redacted>.md) | hall-ui / admission / front door |
352
+ | 0258 | [**The public CLI is a generated client-only package, and its file list is proven by running it** ([task 1003679](https://cloudbongos.com/builders#/task/1003679) · goal 1000054 — *A newcomer can build without the UI*). The core ships private as `@bongos/core` ([ADR 0108](<redacted>.md)), so a newcomer with no credential can install NOTHING and the web hall is the only way in — the owner's words: "how am I supposed to easily take on tasks as a new builder? I should be able to do everything without the UI." Open task 1002025 proposed publishing the core, which ships the SERVER and bypasses [ADR 0099](<redacted>.md)'s redaction pipeline (dormant until ~2026-11). **Decision: generate a separate public `@cloudbongos/cli` from the core and do not publish the core.** Four load-bearing parts. (1) SCOPED NAME, because npm shares one namespace between org names and unscoped packages — the bare `cloudbongos` is unpublishable *precisely because the owner owns that org*, which npm reports as "invalid" and reads like the name is taken; and a package name cannot be created on the npm website at all (it exists on first publish, which is why "Add Existing Package" answered `Forbidden`). (2) THE FILE LIST IS DECLARED AND PROVEN BY BEHAVIOUR, never computed — a static require-closure CANNOT answer this, because `src/module-api.js` is the doorway a module may only import ([ADR 0083](<redacted>.md)) and it *names* every kernel capability, so a static walk sees 33 `src/` files from `start.js` alone where runtime resolves **one** (`api-prefix.js`, which imports nothing) — the same trap as task 1003677's unsatisfiable done-when. So the test packs the tarball, installs it into an empty dir with NO repo, and runs every verb: it may fail for want of a session, never for a missing file. That caught two real holes on its first strengthened run — `clients/bongos-client/index.mjs`, reached by a dynamic `import()` no `require()` walk can see, and a hand-listed `files` array that dropped `clients/` and produced a tarball which installed cleanly then died on first use; both are now build-time refusals, and the `files` array is derived from the manifest. (3) `claim`/`ship`/`dev`/`serve`/`module`/`upgrade`/`onboard`/`doctor`/`exec`/`package-core` are ABSENT and the CLI says WHY plus the next step (`bongos shell` → a cloud box with the full CLI) — a bare `unknown command` teaches nothing, which is the exact failure being fixed. The supported journey is `login` → `start` → `shell`. (4) A REDACTION GATE fails the build closed on non-loopback IPv4 (RFC 5737 doc ranges exempt), token/key shapes, and every domain + owner login readable from the instance's own `config/branding.json` — needles come from host config so the core carries no instance identity ([ADR 0062 §7](<redacted>.md)); `cloudbongos.com` is allowlisted as public by design. No `repository` field while the core repo is private (it would 404 for every user and publish the owner's login for nothing). 31 files, one dependency (`undici`), version independent of the core's ([ADR 0161](<redacted>.md)). `@bongos/client` is VENDORED, not depended on — a public package depending on a private one is uninstallable. **The owner runs `npm publish`; a builder must not.** Task 1002025 is superseded. Rejected: publishing the core; deriving from a static closure (structurally impossible past the doorway); waiting for the mirror (dormant, and this is a build product not a source release); an unscoped name; depending on `@bongos/client`; a degraded `claim`; shipping all of `src/` to make lazy getters safe.](<redacted>.md) | cli / distribution / public surface |
package/docs/file-map.md CHANGED
@@ -167,6 +167,7 @@
167
167
  │ ├── ship.js ← resolve claim as shipped, award credits (`/builder-ship`); also uploads a secret-scrubbed session digest to the corpus (6D.1, ADR 0027); on a dev box runs the sandbox-first review gate before resolving (#927, ADR 0046)
168
168
  │ ├── ship-visual.js ← the OPTIONAL `--visual <image> [--visual-alt "…"]` leg of a ship (task 1003109): screens the file BEFORE the claim resolves (bad input costs no claim), uploads it AFTER (a failed picture never fails a ship)
169
169
  │ ├── package-core.js ← package the core as a versioned, installable artifact + pin manifest (`bongos package-core`; R84, ADR 0100 §1, [#1688](https://example.com/builders#/task/1688)): isPublishable() selection + mirror redaction + fail-closed no-leak gate → dist/bongos-core-<version>.{tgz,manifest.json}; version = src/module-api CORE_VERSION. Recipe: docs/recipes/packaging-the-core.md
170
+ │ ├── build-cli-package.js ← generate the PUBLIC, client-only `@cloudbongos/cli` npm package from this repo (task 1003679, ADR 0258): declared FILES manifest (no server/routes/auth/pool/provisioning) + generated dispatcher, README and package.json whose `files` array is DERIVED from the manifest; refuses to emit on a relative `import()` the manifest omits, and on a redaction hit (non-loopback IPv4, token/key shapes, instance domains + owner login read from config/branding.json). Sibling of package-core.js, opposite audience: that one packages the private core for an instance, this one packages the client surface for the public. Proven by tests/cli_package.mjs, which installs the packed tarball into an empty dir and runs every verb. `npm publish` is the OWNER's step, never a builder's → dist/cloudbongos-cli/
170
171
  │ ├── sandbox-stage.js ← stage the working tree on the live game preview (box OR local) for browser review before ship; resolvePreviewContext picks the context (`/builder-stage`; #927, ADR 0046; local: task 1056)
171
172
  │ ├── local-preview.js ← builder CLI for the LOCAL sandbox: start/stop/status/restart/logs/url of the game-only preview at http://localhost:3100 (`npm run preview`; task 1056)
172
173
  │ ├── local-preview-lib.js ← core of the local sandbox launcher (detached `node src/preview-server.js`, pid/health/port mgmt); shared by local-preview.js + sandbox-stage.js (task 1056)
@@ -1601,5 +1601,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1601
1601
  landed since 1.19.574 with no explicit bump. run 34076465465. (task 1002620)
1602
1602
  1.19.576 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1603
1603
  landed since 1.19.575 with no explicit bump. run 34132732488. (task 1002620)
1604
+ 1.19.577 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1605
+ landed since 1.19.576 with no explicit bump. run 34142767912. (task 1002620)
1606
+ 1.19.578 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1607
+ landed since 1.19.577 with no explicit bump. run 34146835487. (task 1002620)
1604
1608
  ---------------------------------------------------------------------------
1605
1609
  ```
@@ -5590,6 +5590,7 @@ summary{min-height:24px;padding:3px 0;}
5590
5590
  three cards below share ONE read of the row (task 1003582), so
5591
5591
  dropping the memo here is what keeps each entry a fresh read */
5592
5592
  if (!keepRepo) forgetSettings();
5593
+ if (!keepRepo) forgetManifest();
5593
5594
  if (!keepRepo) loadVisibility(inst);
5594
5595
  if (!keepRepo) loadJoinability(inst);
5595
5596
  if (!keepRepo) loadDoor();
@@ -5892,6 +5893,8 @@ summary{min-height:24px;padding:3px 0;}
5892
5893
  if (!inst || !$('mVisRuns')) return;
5893
5894
  if (visPushOpen(inst)) {
5894
5895
  mVisPushSeen = true;
5896
+ forgetManifest(); /* stale from the moment a push is queued — and by the
5897
+ time it settles both cards share one fresh read */
5895
5898
  visRuns(inst.open_intent.state === 'running'
5896
5899
  ? 'Restarting the project to apply a settings change…'
5897
5900
  : 'A settings change is queued — the project restarts to pick it up. Until then it keeps its current setting.');
@@ -5900,43 +5903,81 @@ summary{min-height:24px;padding:3px 0;}
5900
5903
  if (mVisPushSeen) { mVisPushSeen = false; loadVisRuns(inst); }
5901
5904
  }
5902
5905
 
5906
+ /* ── the project's own manifest, read ONCE per manage entry (task 1003554) ─
5907
+ Two cards render off the SAME cross-origin GET of the project's manifest —
5908
+ how the project is reached (brand.project.platformVisibility) and how
5909
+ people join (brand.project.joinability) — and each used to ask for itself:
5910
+ two identical round trips to somebody else's server on every entry.
5911
+
5912
+ Memoised like ensureSettings() and keyed on the project the same way, so a
5913
+ switch is never served the last one's answer. The difference that matters
5914
+ is WHEN it is dropped. The settings row is read once per entry and a memo
5915
+ that lives for that entry is right; this answer instead goes stale the
5916
+ moment a settings push is queued, and the re-read after that push settles
5917
+ is the ONLY thing that can say the new value took. So forgetManifest() is
5918
+ called from the push-open branch of renderVisState/renderJoinState, which
5919
+ runs on every repaint while the push is open and reads nothing itself: by
5920
+ the time it settles the memo is already empty, and the two cards' re-reads
5921
+ then share one read rather than racing two. A lifetime memo here would
5922
+ freeze both lines at their pre-restart value — an efficiency cleanup
5923
+ turned into a wrong answer on screen.
5924
+
5925
+ It RESOLVES with every outcome rather than throwing, so one unreachable
5926
+ project fans out to both lines and neither goes silent: status 0 means it
5927
+ could not be reached at all, any other status is the project's own
5928
+ refusal. The 8s budget and its abort belong to the shared read now — one
5929
+ timer for the pair, not one each. */
5930
+ var manifestPromise = null;
5931
+ var manifestFor = null; /* which project the memo holds — a switch must not reuse it */
5932
+
5933
+ function ensureManifest(domain) {
5934
+ if (manifestPromise && manifestFor === manageId) return manifestPromise;
5935
+ manifestFor = manageId;
5936
+ var ctl = typeof AbortController === 'function' ? new AbortController() : null;
5937
+ var timer = ctl ? setTimeout(function () { ctl.abort(); }, 8000) : null;
5938
+ manifestPromise = fetch('https://' + domain + '/api/gds/instance', { mode: 'cors', signal: ctl ? ctl.signal : undefined })
5939
+ .then(function (res) {
5940
+ /* a project that ANSWERED but refused (its own pre-launch gate, a proxy
5941
+ error page) is not an older core — carry the status, don't guess */
5942
+ if (!res.ok) return { ok: false, status: res.status, data: null };
5943
+ return res.json().then(function (m) { return { ok: true, status: res.status, data: m }; });
5944
+ })
5945
+ .catch(function () { return { ok: false, status: 0, data: null }; })
5946
+ .then(function (ans) { if (timer) clearTimeout(timer); return ans; });
5947
+ return manifestPromise;
5948
+ }
5949
+
5950
+ /* Drop the memo so the next ensureManifest() is a real read — at each manage
5951
+ entry, and for the whole time a settings push is open. */
5952
+ function forgetManifest() { manifestPromise = null; manifestFor = null; }
5953
+
5903
5954
  /* what the project RUNS: its own public manifest, read cross-origin (the
5904
5955
  same GET /instance the platform's runner reads after a restart) */
5905
5956
  function loadVisRuns(inst) {
5906
5957
  if (!inst || inst.status !== 'active' || !inst.domain) { visRuns(''); return; }
5907
5958
  var asked = manageId;
5908
5959
  visRuns('Checking what the project is running…');
5909
- var ctl = typeof AbortController === 'function' ? new AbortController() : null;
5910
- var timer = ctl ? setTimeout(function () { ctl.abort(); }, 8000) : null;
5911
- fetch('https://' + inst.domain + '/api/gds/instance', { mode: 'cors', signal: ctl ? ctl.signal : undefined })
5912
- .then(function (res) {
5913
- /* a project that ANSWERED but refused (its own pre-launch gate, a proxy
5914
- error page) is not an older core — say what happened, not a guess */
5915
- if (!res.ok) throw new Error('http:' + res.status);
5916
- return res.json();
5917
- })
5918
- .then(function (m) {
5919
- if (asked !== manageId) return;
5920
- var p = m && m.brand && m.brand.project;
5921
- var v = p && p.platformVisibility;
5922
- if (!p || typeof p !== 'object') {
5923
- visRuns('This project’s server doesn’t report this setting yet — it runs an older platform version, so the setting can’t take effect until the project is updated.');
5924
- return;
5925
- }
5926
- var o = VIS_OPTIONS[v];
5927
- if (!o) { visRuns('This project’s server reports a setting this page doesn’t recognise (' + esc(String(v)) + ').'); return; }
5928
- var want = mVisSettings && mVisSettings.value;
5929
- visRuns('Right now the project runs as <b>' + esc(o.label) + '</b>' +
5930
- (want && want !== v && VIS_OPTIONS[want] ? ' — the saved choice (' + esc(VIS_OPTIONS[want].label) + ') hasn’t taken effect there yet.' : '.'));
5931
- })
5932
- .catch(function (err) {
5933
- if (asked !== manageId) return;
5934
- var http = /^http:(\d+)$/.exec(String(err && err.message));
5935
- visRuns(http
5936
- ? 'The project answered but didn’t share what it’s running (it replied ' + esc(http[1]) + ').'
5960
+ ensureManifest(inst.domain).then(function (ans) {
5961
+ if (asked !== manageId) return;
5962
+ if (!ans.ok) {
5963
+ visRuns(ans.status
5964
+ ? 'The project answered but didn’t share what it’s running (it replied ' + esc(String(ans.status)) + ').'
5937
5965
  : 'Couldn’t reach the project to check what it’s running right now.');
5938
- })
5939
- .then(function () { if (timer) clearTimeout(timer); });
5966
+ return;
5967
+ }
5968
+ var m = ans.data;
5969
+ var p = m && m.brand && m.brand.project;
5970
+ var v = p && p.platformVisibility;
5971
+ if (!p || typeof p !== 'object') {
5972
+ visRuns('This project’s server doesn’t report this setting yet — it runs an older platform version, so the setting can’t take effect until the project is updated.');
5973
+ return;
5974
+ }
5975
+ var o = VIS_OPTIONS[v];
5976
+ if (!o) { visRuns('This project’s server reports a setting this page doesn’t recognise (' + esc(String(v)) + ').'); return; }
5977
+ var want = mVisSettings && mVisSettings.value;
5978
+ visRuns('Right now the project runs as <b>' + esc(o.label) + '</b>' +
5979
+ (want && want !== v && VIS_OPTIONS[want] ? ' — the saved choice (' + esc(VIS_OPTIONS[want].label) + ') hasn’t taken effect there yet.' : '.'));
5980
+ });
5940
5981
  }
5941
5982
 
5942
5983
  /* ── the settings row, read ONCE per manage entry (task 1003582) ─────────
@@ -6305,6 +6346,7 @@ summary{min-height:24px;padding:3px 0;}
6305
6346
  if (!inst || !$('mJoinRuns')) return;
6306
6347
  if (visPushOpen(inst)) {
6307
6348
  mJoinPushSeen = true;
6349
+ forgetManifest(); /* the same one settle, the same one drop */
6308
6350
  joinRuns(inst.open_intent.state === 'running'
6309
6351
  ? 'Restarting the project to apply a settings change…'
6310
6352
  : 'A settings change is queued — the project restarts to pick it up. Until then it keeps its current setting.');
@@ -6313,40 +6355,33 @@ summary{min-height:24px;padding:3px 0;}
6313
6355
  if (mJoinPushSeen) { mJoinPushSeen = false; loadJoinRuns(inst); }
6314
6356
  }
6315
6357
 
6316
- /* what the project RUNS: its own public manifest, read cross-origin */
6358
+ /* what the project RUNS: the SAME shared manifest read the reach card uses
6359
+ (task 1003554) — a different key out of one answer, not a second GET */
6317
6360
  function loadJoinRuns(inst) {
6318
6361
  if (!inst || inst.status !== 'active' || !inst.domain) { joinRuns(''); return; }
6319
6362
  var asked = manageId;
6320
6363
  joinRuns('Checking what the project is running…');
6321
- var ctl = typeof AbortController === 'function' ? new AbortController() : null;
6322
- var timer = ctl ? setTimeout(function () { ctl.abort(); }, 8000) : null;
6323
- fetch('https://' + inst.domain + '/api/gds/instance', { mode: 'cors', signal: ctl ? ctl.signal : undefined })
6324
- .then(function (res) {
6325
- if (!res.ok) throw new Error('http:' + res.status);
6326
- return res.json();
6327
- })
6328
- .then(function (m) {
6329
- if (asked !== manageId) return;
6330
- var p = m && m.brand && m.brand.project;
6331
- var v = p && p.joinability;
6332
- if (!p || typeof p !== 'object') {
6333
- joinRuns('This project’s server doesn’t report this setting yet — it runs an older platform version, so the setting can’t take effect until the project is updated.');
6334
- return;
6335
- }
6336
- var o = JOIN_OPTIONS[v];
6337
- if (!o) { joinRuns('This project’s server reports a setting this page doesn’t recognise (' + esc(String(v)) + ').'); return; }
6338
- var want = mJoinSettings && mJoinSettings.value;
6339
- joinRuns('Right now the project runs as <b>' + esc(o.label) + '</b>' +
6340
- (want && want !== v && JOIN_OPTIONS[want] ? ' — the saved choice (' + esc(JOIN_OPTIONS[want].label) + ') hasn’t taken effect there yet.' : '.'));
6341
- })
6342
- .catch(function (err) {
6343
- if (asked !== manageId) return;
6344
- var http = /^http:(\d+)$/.exec(String(err && err.message));
6345
- joinRuns(http
6346
- ? 'The project answered but didn’t share what it’s running (it replied ' + esc(http[1]) + ').'
6364
+ ensureManifest(inst.domain).then(function (ans) {
6365
+ if (asked !== manageId) return;
6366
+ if (!ans.ok) {
6367
+ joinRuns(ans.status
6368
+ ? 'The project answered but didn’t share what it’s running (it replied ' + esc(String(ans.status)) + ').'
6347
6369
  : 'Couldn’t reach the project to check what it’s running right now.');
6348
- })
6349
- .then(function () { if (timer) clearTimeout(timer); });
6370
+ return;
6371
+ }
6372
+ var m = ans.data;
6373
+ var p = m && m.brand && m.brand.project;
6374
+ var v = p && p.joinability;
6375
+ if (!p || typeof p !== 'object') {
6376
+ joinRuns('This project’s server doesn’t report this setting yet — it runs an older platform version, so the setting can’t take effect until the project is updated.');
6377
+ return;
6378
+ }
6379
+ var o = JOIN_OPTIONS[v];
6380
+ if (!o) { joinRuns('This project’s server reports a setting this page doesn’t recognise (' + esc(String(v)) + ').'); return; }
6381
+ var want = mJoinSettings && mJoinSettings.value;
6382
+ joinRuns('Right now the project runs as <b>' + esc(o.label) + '</b>' +
6383
+ (want && want !== v && JOIN_OPTIONS[want] ? ' — the saved choice (' + esc(JOIN_OPTIONS[want].label) + ') hasn’t taken effect there yet.' : '.'));
6384
+ });
6350
6385
  }
6351
6386
 
6352
6387
  function loadJoinability(inst) {
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.576",
3
+ "version": "1.19.578",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.576",
9
+ "version": "1.19.578",
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.576",
3
+ "version": "1.19.578",
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",
@@ -124,7 +124,7 @@ function knownHostsLineNamesAny(line, names) {
124
124
  // line for the same box.
125
125
  //
126
126
  // It used to compare field 1 to the hostname with `!==`, which missed the two forms
127
- // OpenSSH actually writes — `host,1.2.3.4` (what `StrictHostKeyChecking=accept-new`
127
+ // OpenSSH actually writes — `host,192.0.2.4` (what `StrictHostKeyChecking=accept-new`
128
128
  // records) and the hashed `|1|salt|hash`. Those lines survived the "replace", so
129
129
  // every re-pin ACCUMULATED another entry and the client kept verifying against the
130
130
  // first, now-wrong one: "Host denied (verification failed)" on a box that had just