@bongos/core 1.19.368 → 1.19.369
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 +34 -19
- package/docs/adr/0192-platform-visibility-member-door.md +50 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +158 -157
- package/docs/copy-registry.json +206 -197
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/projects.html +201 -3
- package/src/bongos/auth.js +17 -5
- package/src/bongos/platform-visibility-gate.js +251 -0
- package/src/bongos/serve-internal.js +10 -0
- package/src/module-api.js +3 -1
- package/tests/platform_visibility_gate.mjs +280 -0
- package/tests/projects_hub.mjs +93 -0
- package/tests/projects_hub_pre_uat.mjs +4 -2
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.369",
|
|
6
|
+
"core_contract": "1.19.369",
|
|
7
|
+
"source_commit": "486cdf89329ccbf01188586ff918d22390620cba",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-25T21:
|
|
9
|
+
"built_at": "2026-08-25T21:52:45.667Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 333,
|
|
13
13
|
"agent_docs_stubbed": 22,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1543,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1898,
|
|
20
|
+
"tree_sha256": "e4c29c0518ebcdb414195829f5e8efd9f175df0feb556f1eb8114b2f921e2336",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1454,10 +1454,15 @@
|
|
|
1454
1454
|
"mode": "0000644",
|
|
1455
1455
|
"sha256": "4cb1023731d70965f544bfcde1a9bfa3165e2bc96e0825c7e2c3b5a02c464a63"
|
|
1456
1456
|
},
|
|
1457
|
+
{
|
|
1458
|
+
"path": "docs/adr/0192-platform-visibility-member-door.md",
|
|
1459
|
+
"mode": "0000644",
|
|
1460
|
+
"sha256": "caf369cc73f2a997df7dd12b074a5e7c815b8f717cb864ce49ce533d74008d75"
|
|
1461
|
+
},
|
|
1457
1462
|
{
|
|
1458
1463
|
"path": "docs/adr/README.md",
|
|
1459
1464
|
"mode": "0000644",
|
|
1460
|
-
"sha256": "
|
|
1465
|
+
"sha256": "632a5dd2b047437eca5201d0e572378a2a70d0b5d3ff10e97285ee00eec49618"
|
|
1461
1466
|
},
|
|
1462
1467
|
{
|
|
1463
1468
|
"path": "docs/api-reference.md",
|
|
@@ -1492,12 +1497,12 @@
|
|
|
1492
1497
|
{
|
|
1493
1498
|
"path": "docs/copy-inventory.md",
|
|
1494
1499
|
"mode": "0000644",
|
|
1495
|
-
"sha256": "
|
|
1500
|
+
"sha256": "e2c301d24dae22625ba110dc671a5cfc861fd1ae4100bdefd0e563857279fc94"
|
|
1496
1501
|
},
|
|
1497
1502
|
{
|
|
1498
1503
|
"path": "docs/copy-registry.json",
|
|
1499
1504
|
"mode": "0000644",
|
|
1500
|
-
"sha256": "
|
|
1505
|
+
"sha256": "9e3b3f18ba94a2fd2fd35c02403aa45f3ff7a4dd5268dec720bb6b211666e6f9"
|
|
1501
1506
|
},
|
|
1502
1507
|
{
|
|
1503
1508
|
"path": "docs/design-contract.md",
|
|
@@ -4892,12 +4897,12 @@
|
|
|
4892
4897
|
{
|
|
4893
4898
|
"path": "package-lock.json",
|
|
4894
4899
|
"mode": "0000644",
|
|
4895
|
-
"sha256": "
|
|
4900
|
+
"sha256": "7ecd5aa9734efaab265e867498834dae2a5a58e4c0d1616cddcb0cfcea5b17e3"
|
|
4896
4901
|
},
|
|
4897
4902
|
{
|
|
4898
4903
|
"path": "package.json",
|
|
4899
4904
|
"mode": "0000644",
|
|
4900
|
-
"sha256": "
|
|
4905
|
+
"sha256": "4463e81c49fd1c7476e9351255bb9d37516106d9d5e2dc88ad0baaabf521de5e"
|
|
4901
4906
|
},
|
|
4902
4907
|
{
|
|
4903
4908
|
"path": "public-docs/index.html",
|
|
@@ -5157,7 +5162,7 @@
|
|
|
5157
5162
|
{
|
|
5158
5163
|
"path": "public-landing/projects.html",
|
|
5159
5164
|
"mode": "0000644",
|
|
5160
|
-
"sha256": "
|
|
5165
|
+
"sha256": "38df756a4d5bac76ea0369f81392d32d906f5f16a634c97311457779c15b667f"
|
|
5161
5166
|
},
|
|
5162
5167
|
{
|
|
5163
5168
|
"path": "public-landing/world/assets/clouds/cloud-island-large.png",
|
|
@@ -6532,7 +6537,7 @@
|
|
|
6532
6537
|
{
|
|
6533
6538
|
"path": "src/bongos/auth.js",
|
|
6534
6539
|
"mode": "0000644",
|
|
6535
|
-
"sha256": "
|
|
6540
|
+
"sha256": "c7f087b10ffc9288ec61e8b4b4c17e089b9e137da03961a1c71a62a4099c2ae7"
|
|
6536
6541
|
},
|
|
6537
6542
|
{
|
|
6538
6543
|
"path": "src/bongos/cascade-dispatch.js",
|
|
@@ -6629,6 +6634,11 @@
|
|
|
6629
6634
|
"mode": "0000644",
|
|
6630
6635
|
"sha256": "5116468286f737a3421dbcdfdc48f78ecc8f25496f7db0382f9af9e1a0110e9c"
|
|
6631
6636
|
},
|
|
6637
|
+
{
|
|
6638
|
+
"path": "src/bongos/platform-visibility-gate.js",
|
|
6639
|
+
"mode": "0000644",
|
|
6640
|
+
"sha256": "b1b950e19c072eb88cc0fc928efbfb3fde2c759dd9c59ebc28445703c0336591"
|
|
6641
|
+
},
|
|
6632
6642
|
{
|
|
6633
6643
|
"path": "src/bongos/pool.js",
|
|
6634
6644
|
"mode": "0000644",
|
|
@@ -6742,7 +6752,7 @@
|
|
|
6742
6752
|
{
|
|
6743
6753
|
"path": "src/bongos/serve-internal.js",
|
|
6744
6754
|
"mode": "0000644",
|
|
6745
|
-
"sha256": "
|
|
6755
|
+
"sha256": "5b2c35e1c4a9b75616adcc4aacd851fc16885e7e9a919c4becc67efb44cd44f2"
|
|
6746
6756
|
},
|
|
6747
6757
|
{
|
|
6748
6758
|
"path": "src/bongos/stealth-gate.js",
|
|
@@ -6777,7 +6787,7 @@
|
|
|
6777
6787
|
{
|
|
6778
6788
|
"path": "src/module-api.js",
|
|
6779
6789
|
"mode": "0000644",
|
|
6780
|
-
"sha256": "
|
|
6790
|
+
"sha256": "8f018f2f5c96b98e7e08bfedb50161b4b9ffb639afe2ab9f108541e951c3fc6e"
|
|
6781
6791
|
},
|
|
6782
6792
|
{
|
|
6783
6793
|
"path": "src/module-loader/catalog.js",
|
|
@@ -8644,6 +8654,11 @@
|
|
|
8644
8654
|
"mode": "0000644",
|
|
8645
8655
|
"sha256": "3552e9e4e947c21770d734b340893db576ea42c7b5de9d2822021a03e1345810"
|
|
8646
8656
|
},
|
|
8657
|
+
{
|
|
8658
|
+
"path": "tests/platform_visibility_gate.mjs",
|
|
8659
|
+
"mode": "0000644",
|
|
8660
|
+
"sha256": "2dc7a56caeababd5bf249e968e4353f8c738aef459947fde15c7c511640c13aa"
|
|
8661
|
+
},
|
|
8647
8662
|
{
|
|
8648
8663
|
"path": "tests/post_login_landing.mjs",
|
|
8649
8664
|
"mode": "0000644",
|
|
@@ -8697,12 +8712,12 @@
|
|
|
8697
8712
|
{
|
|
8698
8713
|
"path": "tests/projects_hub.mjs",
|
|
8699
8714
|
"mode": "0000644",
|
|
8700
|
-
"sha256": "
|
|
8715
|
+
"sha256": "7ced3851eb30960017693dfc0fd4711164448cfde17448ba92d528bdd3091041"
|
|
8701
8716
|
},
|
|
8702
8717
|
{
|
|
8703
8718
|
"path": "tests/projects_hub_pre_uat.mjs",
|
|
8704
8719
|
"mode": "0000644",
|
|
8705
|
-
"sha256": "
|
|
8720
|
+
"sha256": "bb0da311738faba71a077557e60b5ab30086399ac767e677baf53334ca769c29"
|
|
8706
8721
|
},
|
|
8707
8722
|
{
|
|
8708
8723
|
"path": "tests/promote_goal_id.mjs",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# ADR 0192 — A gated project refuses strangers at the app's front door, and the hub reads what the project runs from the project itself
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-08-25
|
|
5
|
+
- **Task:** [task 1003043](https://cloudbongos.com/builders#/task/1003043) (goal 1000058 — the non-cloudbongos project lifecycle)
|
|
6
|
+
- **Relates to:** ADR 0190 (the settings channel: in-place env patch + restart; `brand.project` as the read-back), ADR 0016 (the DB enforces, markdown describes), ADR 0116 (the error envelope), ADR 0182 §7 (the word `stealth`; the infra gate is the *pre-launch* gate), task 1003040 (the settings row), task 1003139 (`project.*` from web.env), task 1002775 (the pre-launch gate)
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
Task 1003040 gave every project a `platform_visibility` setting (`public` | `gated`) on the platform's row; 1003139 and 1003140 carried it to the instance's environment and into `branding().project.platformVisibility`, resolved but **enforced by nothing**. The owner decided on 2026-08-16 that platform visibility and repository visibility are separate settings, and the task asks for server-side enforcement on the instance — "checked per request, not CSS hiding" — with the sign-in door still reachable so members can get in, and a manage-page toggle on the hub that says what the project runs rather than what was asked.
|
|
11
|
+
|
|
12
|
+
Two shapes were on the table for the instance side. The hall's page gates (`runPageGate`, ADR 0151) answer a stranger with a `302` into the OAuth round trip, and are wired per HTML surface in `serve-internal.js`. The pre-launch gate (`stealth-gate.js`) is one app-level middleware ahead of everything, with a short exempt list, and answers with a self-contained `401` page.
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
1. **One app-level middleware, mounted with the pre-launch gate — not per-surface wiring.** `src/bongos/platform-visibility-gate.js` mounts in `mountInternalSurfaces` right after `attachFail`, ahead of every human and public-read surface: the landing, `/projects`, `/create`, the status dashboard host, the hall shell and its assets, the `/public/*` read API and every module's `rank: public` route. The public surfaces are registered in a dozen places across the core and the modules; gating them one by one is the shape that leaks the next one somebody adds. A middleware ahead of all of them cannot miss a surface it has never heard of, which is the pre-launch gate's argument and it holds here.
|
|
17
|
+
|
|
18
|
+
2. **"Member" is a live, non-deactivated session on this instance — the same lookup `requireBuilder` uses.** The gate calls `resolveSession(req)` per request; there is no cache and no second store. A Xenos is a member (they signed in; the rank ladder gates what they can *do*, not whether they may see the door they walked through). A builder with `status = 'inactive'` is refused (the task 871 hot-path rule), and only on that known value. A lookup that throws **fails closed**: the caller's identity is unknown, so admitting is never an option — the rule `requireBuilder` and `runPageGate` already hold.
|
|
19
|
+
|
|
20
|
+
3. **The exempt list is the door, the manifest, the probes and the downloads — on every API prefix — and nothing wider.** On each of `ALL_API_PREFIXES` (`/api/bongos/v1`, `/api/bongos`, `/api/gds` — a CLI pinned to the versioned contract, ADR 0117, must find the door where the unversioned one is): `auth/*` (the web round trip, the CLI device flow, sign-out: a door that cannot be opened locks the members out too); `instance` (the public manifest a stranger reads to learn *how* to sign in, and — load-bearing — what the platform's runner reads after a settings restart to learn whether the value was consumed, `provision-units.js settingsConsumed`, including the very push that turns this gate on); `healthz`; `provisioning/tls-check` and `provisioning/health` (Caddy's on-demand-TLS *ask* and the provisioning probe — server-to-server and cookie-less; on a control plane a refused ask means no project ever gets a certificate). Plus `/healthz`, `/version`, `/metrics` (the deploy's, the uptime poller's and the liveness sweep's probes), `/downloads/*` (the installer script and the Dev Box updater — a cookie-less `curl … | sh` must not be handed an HTML door), and `/__stealth`. The test sees the path the way Express routes it — lowercased, one trailing slash stripped — so a probe spelled `/HEALTHZ` or `/api/gds/instance/` is not refused where the router would have served it; mismatches still fail closed. A CORS preflight passes — it carries no identity and admits nothing. The pre-launch gate's blanket `/api/` exemption is deliberately **not** copied: the task names the hall read endpoints as surfaces to gate. The `auth/*` subtree is exempt whole, not path by path: a missed sign-in path is a locked door for every member, the worse failure; the cost is that the admission-status lookup (public on every instance today, its own comment noting it reveals account existence) stays reachable on a gated one — throttling it is its own task.
|
|
21
|
+
|
|
22
|
+
4. **A refused page gets a `401` door; a refused API call gets the `401` envelope.** The door is self-contained (no stylesheet, no branding bundle, no script — nothing that shows the design to whoever knocks), names the project, says who it is for, and carries one control: *Sign in*, linking to `/api/gds/auth/web/start?return=<the page they knocked on>` — a **relative** return (the pre-launch gate's `relativeReturn` rule: one leading slash, no `//`, no backslash), because `safeReturnTo` honours a relative path unconditionally while an absolute one only over https on the cookie apex, so an absolute return would silently drop a member on a co-located subdomain or a dev port at the default page (GET/HEAD only; a write returns to `/`). A `302` straight into GitHub was rejected: every stray asset request and every crawler would be sent to an OAuth screen, and a `401` tells a crawler that ignores the `noindex` what happened. An API refusal is `res.fail('unauthenticated', 401)` — exactly what `requireBuilder` emits — so a client's existing 401 handling reads it; mounting after `attachFail` is what makes that possible. Both carry `Cache-Control: no-store` and `Vary: Cookie`.
|
|
23
|
+
|
|
24
|
+
5. **The hub's manage page shows the CHOICE from the platform's row and what the project RUNS from the project's own manifest — and never confuses the two.** The radio group is seeded from `GET /provisioning/instances/:id/settings` (the platform's stored request). The line beneath it is written only by a cross-origin read of `https://<domain>/api/gds/instance` → `brand.project.platformVisibility` — the same public read the runner performs (decision 3 is what keeps it answering when gated) — and says one of: *runs as X*, *runs as X — the saved choice hasn't taken effect there yet*, *doesn't report this setting yet (an older platform version)*, or *couldn't reach the project*. The save's `push.message` is rendered verbatim and its `saved` moves only the radio, never the "runs" line (ADR 0190 §7: the reply is a receipt). While a `settings-apply` intent is open the line reads the intent's state (fetch-free, on the card's tick); the moment it is gone the manifest is asked again, because only the manifest can say the value took.
|
|
25
|
+
|
|
26
|
+
6. **The setting is read through `branding()`, memoized for the process lifetime — and an unreadable pack fails CLOSED.** Per-request `branding()` is a frozen-object read, not a file read. A change is applied by the restart the runner performs (ADR 0190) — the gate never re-reads a file, and the hub never claims the gate is on until the project's own manifest says so. If the pack cannot be read at all the gate treats the project as gated (logged): it is the one place a broken read must not quietly un-gate a members-only project, and every other branding consumer on the instance is broken by then anyway, so refusing costs nothing a stranger was owed.
|
|
27
|
+
|
|
28
|
+
7. **A member's answer leaves marked private, whatever the route said.** The public read API and the hall assets send `Cache-Control: public, max-age=…`; a standalone project sits behind a proxied CDN edge that honours `public` for everyone and ignores `Vary` on anything but `Accept-Encoding`. A member's request would populate that edge with a copy the next stranger is served without the request ever reaching Node — a bypass no per-request check can see. So on the pass path the gate patches `res.writeHead` to change the **scope** of `Cache-Control` on the way out, not its freshness: `public` → `private`, `s-maxage` dropped, the route's `max-age` kept, `no-store` left alone, and a response that named no policy gets `private, no-cache`; `Cookie` is added to `Vary` (implicit heads — `res.end`, streams — go through `writeHead` too). Keeping the freshness matters: a hall page is thirty assets and, on a gated project, each re-fetch is a session lookup — `private, no-store` would have closed the edge leak by making every member reload everything from origin. Exempt paths keep their own headers: the manifest is public by design.
|
|
29
|
+
|
|
30
|
+
8. **A request's session is resolved once.** The gate asks `resolveSession` ahead of every surface and the page/API gates ask again behind it; each lookup is a SELECT plus a last-seen UPDATE on the session row, so a member's page load with thirty assets would pay sixty. `resolveSession` now memoizes per request (a `WeakMap` keyed on `req`, sharing a rejection too, so every gate on one request fails closed the same way).
|
|
31
|
+
|
|
32
|
+
## Consequences
|
|
33
|
+
|
|
34
|
+
- Flipping a live project to *Only signed-in members* on the hub, once its own core carries this change, makes every one of its surfaces answer a stranger with the door; members see everything they saw before. The hub shows the platform's choice immediately and the project's reality only when the project reports it.
|
|
35
|
+
- A project whose pinned core predates this change receives the setting (1003139/1003140) and **enforces nothing** — the manage page says so in the "runs" line ("doesn't report this setting yet") instead of implying the door is up. The live test project (Spike Harbor) ran core 1.19.2 when this landed; the enforcement half of the done-when is witnessed only after its pin is raised.
|
|
36
|
+
- A gated instance's public read API is closed to strangers, so any external consumer of `/api/*/public/*` (a badge, a status embed) on a gated project needs a session. That is the setting's meaning, not a side effect.
|
|
37
|
+
- The control plane itself can be gated by the same setting; nothing in the gate knows which instance it runs on.
|
|
38
|
+
|
|
39
|
+
## Rejected
|
|
40
|
+
|
|
41
|
+
- **Per-surface wiring** (the hall page-gate pattern, one line per surface in `serve-internal.js`) — misses every surface it was not told about; the public read API alone is spread over six modules.
|
|
42
|
+
- **A `302` into sign-in** for a refused page — sends assets and crawlers to OAuth; the door page with the link is the same click for a member and honest for everyone else.
|
|
43
|
+
- **Copying the pre-launch gate's `/api/` exemption** — leaves the hall read endpoints open, which the task names as surfaces to gate.
|
|
44
|
+
- **Painting the hub's radio from the manifest** (or the "runs" line from `saved`) — the two answers differ by design for the whole life of a push, and collapsing them is the F16 lie in either direction.
|
|
45
|
+
- **Renaming or reusing the pre-launch gate** — a shared password for an unannounced instance and a per-project members-only policy are different doors with different keys; ADR 0182 §7 already tracks the pre-launch gate's rename separately.
|
|
46
|
+
- **Exempting the sign-in paths one by one** instead of the `auth/*` subtree — see decision 3; a missed path locks members out.
|
|
47
|
+
|
|
48
|
+
## Deferred (review round, 2026-08-25)
|
|
49
|
+
|
|
50
|
+
- **Projecting the manifest down when gated.** `GET /instance` still carries the full `clientBranding`, `modules`, and `repo: {owner, repo, branch, url}` — the repository's coordinates, even when the owner set it private on the same page. Sign-in and the read-back need only `schema`, `name`, `auth`, `admission`, `brand.project` (+ the product name for the door). That trims a protected route and its pinned schema; captured as idea 1000845 for its own task.
|
package/docs/adr/README.md
CHANGED
|
@@ -268,6 +268,7 @@ This keeps the decision history honest and traceable.
|
|
|
268
268
|
| 0190 | [**A changed project setting reaches a running instance by an in-place env patch and a restart — never a template re-write** ([task 1003140](https://cloudbongos.com/builders#/task/1003140), goal 1000058). Task 1003139 made a NEW instance read `platform_visibility` / `joinability` from the web.env the runner writes; a RUNNING instance loads its branding pack once, memoized and deep-frozen for the process lifetime, so a changed value could not reach it at all — the settings were write-only state on the platform. Decision: `PATCH /provisioning/instances/:id/settings` writes the platform row first, then for an ACTIVE instance on a shape the runner can reach enqueues a `settings-apply` intent (migration provisioning_010); the control-plane runner reads the instance's web.env as it stands, upserts ONLY the two settings lines (`upsertEnvVars`), and RESTARTS the service. The in-place patch is the load-bearing choice: the natural move — re-running `webEnvBody` as domain-attach does — regenerates the file from the row, and the row does not know the OAuth client secret or the App private key that `placeManifestCreds` placed later, so a template re-write silently blanks sign-in on the very instance the owner just touched. `SETTINGS_APPLY_FROM = {active}` because a pre-active instance gets the same values at standup and a torn-down one has nothing to restart (the provision intent cannot stand in — it excludes `active`, task 1002691); `SETTINGS_APPLY_SHAPES = {co-tenant, standalone}` because a dedicated droplet has no post-hoc exec path, and a push that cannot land is refused at the route AND the runner rather than reported. The response is a receipt, not a claim: it says the restart is coming and that the instance keeps its current setting until then, and the read-back lives on the instance's own public `GET /instance` manifest, whose `brand.project` now carries the resolved policy (`clientBranding` allow-list widened deliberately). Rejected: the template re-write; re-provisioning as the push; skipping the restart (impossible while the pack is memoized); claiming the new state on PATCH return.](<redacted>.md) | provisioning / settings channel |
|
|
269
269
|
|
|
270
270
|
| 0191 | [**Majority, and an author who may vote but never decide alone** ([task 1003273](https://cloudbongos.com/builders#/task/1003273); diagnosis and companion in [task 1003271](https://cloudbongos.com/builders#/task/1003271)). Supersedes ADR 0175 §3's pass-rule set and §§7/11's flat anti-self rule. The owner reported voting as broken; nothing was broken. Nine items sat open under `first_ratifier` with four archons active — any one of three non-authors could have ratified any of them in a click — but every item was authored by the one person looking at them, and ADR 0175's anti-self rule was enforced by rendering NO BALLOT, so from the owner's seat the feature did not exist. The lesson: a rule enforced by removing the control is indistinguishable from a broken feature. Two decisions. (1) `majority` joins PASS_RULES: strictly more than half of the resolved MEMBERSHIP must vote yes — of the membership, not of the votes cast, so low turnout cannot quietly pass things; a tie is not a majority. The three rules are NOT a ladder of strictness, they differ on WHAT SILENCE MEANS — `first_ratifier` and `consent` pass an unvoted item, `majority` returns it — which is the axis that matters and now the comment in config.js. (2) An author MAY vote and may NEVER DECIDE ALONE: the rule moved from refusing the cast to the two places something is actually decided — `shouldCloseAfterVote` needs a non-author vote under every rule, and the majority tally needs a non-author YES. Their yes still counts toward the numerator; their `no` is excluded from the objection test (else silence would be an author's safest act); a self-cast earns no karma. The ADR 0175 §7 amendment carve-out survives as the author rule's SCOPE, not an exception: `authorRuleApplies` is full_idea-only, so a sole Archon still ratifies their own amendment. Also records why the CLOCK is a consequence of majority and not an independent fix — `tallyOutcome` passes on ZERO votes, so adding a deadline under `first_ratifier` would have closed all nine items as ratified-unread, paying karma and credits on ideas nobody read; `BOARD_DEFAULTS.window_minutes` therefore stays `null` in the neutral core. States the cost plainly: three affirmative votes are needed on a four-member board, and a solo instance still cannot ratify its own ideas. Discord stays outbound-only (ADR 0175 §8 re-affirmed).](<redacted>.md) | government / board room |
|
|
271
|
+
| 0192 | [**A gated project refuses strangers at the app's front door, and the hub reads what the project runs from the project itself** ([task 1003043](https://cloudbongos.com/builders#/task/1003043), goal 1000058). Task 1003040 gave every project a `platform_visibility` setting and 1003139/1003140 carried it into the instance's `branding().project.platformVisibility` — resolved, enforced by nothing. Decision: ONE app-level middleware (`src/bongos/platform-visibility-gate.js`), mounted beside the pre-launch gate ahead of every human and public-read surface — the landing, the hub pages, the status host, the hall shell, `/public/*` and every module's `rank: public` route — rather than per-surface wiring, which misses the next surface nobody told it about. "Member" is a live, non-deactivated session on this instance, the same `resolveSession` lookup `requireBuilder` uses, per request, no cache; a lookup that throws fails CLOSED. The exempt list is the door, the manifest and the probes and nothing wider: `/api/*/auth/*` (a door that cannot be opened locks the members out too), `/api/*/instance` (what a stranger reads to learn how to sign in AND what the runner reads after a settings restart to learn whether the value was consumed — including the push that turns this gate on), `/healthz`, `/version`, `/metrics`; the pre-launch gate's blanket `/api/` exemption is deliberately not copied. A refused page gets a self-contained 401 door (no stylesheet, no branding bundle, no script) with one control — Sign in, returning (relatively) to the page knocked on; a refused API call gets `res.fail('unauthenticated', 401)`, exactly requireBuilder's envelope, which is why it mounts after `attachFail`. A member's answer leaves marked `private, no-store` whatever the route said — the public read API sends `public, max-age`, and a CDN edge would otherwise serve a member's copy to the next stranger. The exempt list covers every API prefix (the versioned contract too), `/downloads/*` and Caddy's TLS ask; a request's session is resolved once (`resolveSession` memoizes per request). The hub's manage page shows the CHOICE from the platform's row and what the project RUNS from the project's own public manifest (`brand.project`), and never confuses the two: `push.message` is rendered verbatim, `saved` moves only the radio, the "runs" line is written only from the manifest and says "doesn't report this setting yet" for a core that predates the channel. Rejected: per-surface wiring; a 302 into OAuth for a refused page (sends assets and crawlers to GitHub); copying the `/api/` exemption; painting the radio from the manifest or the runs line from `saved`; reusing the pre-launch gate.](<redacted>.md) | instance / project visibility |
|
|
271
272
|
| 0192 | [**A work category orients a human and authorises nothing** ([task 1003275](https://cloudbongos.com/builders#/task/1003275), goal 1000072, criterion `legibility-categories`). A goal gains a human CATEGORY from an owner-named set of eight (Builders Hall, Context Management, Government (Decision Making), Modules, Security, Economics, Legal, User/ABI Experience) so a person scanning the board can tell at a glance which goals are about money, which about the walls, which about the hall — today each goal's own words say so, but only if you read all thirty-eight. The DECISION is the constraint, not the feature: the owner said twice that this organisation has no hierarchical purpose and no effect on the agentic management of the work, and the system ALREADY has a load-bearing hierarchy (ADR 0086 — the scope_modules wall, rank floors, goal membership). The goal's own prose predicts the failure: a second thing that looks like a hierarchy gets mistaken for one within a release, and the mistake arrives as ONE plausible line in a gate ('skip Legal goals in the feed'), after which work is refused because of a label nobody meant as a permission. So: no authority, claim-eligibility, feed or dependency surface may read a category — enforced by the `scripts/gds/category-advisory-guard.js` fitness check, which scans every decision surface with comments stripped (a gate may CITE the rule, never branch on it) AND asserts the feature still exists, so a renamed-away subject fails loudly instead of passing vacuously (the ADR 0022 assurance-of-assurance finding). Uncategorised is LEGAL — nullable, no default, required by no write — because the criterion is explicit that Claude failing to place a goal is a designed prompt, not an error. A task stores no category and INHERITS its goal's through `tasks.goal_id`; a stored copy would be a second answer to one question. The set is a table, not an enum (runtime-extensible without a migration per addition) and retiring is `active = false`, never a DELETE, so a goal keeps rendering a label someone tidied away. The three route-rank pins are a tripwire: if one ever needs a WIDER gate than its neighbours, the label has grown into a permission. Deliberately NOT built: a per-category page or filtered navigation view — a chip is a label, a drill-down destination is a tier. Making a category gate anything is an owner decision and needs its own ADR first.](<redacted>.md) | GDS / goals |
|
|
272
273
|
|
|
273
274
|
> ⚠️ **Numbering collision (known):** two ADRs share `0008`. The numbers were assigned in parallel sessions before the lock could be enforced. The files are kept under their existing filenames to preserve git history, but the table disambiguates them as `0008-a` / `0008-b`. New ADRs should resume at `0040` to avoid further collisions. (`0024` is also a real on-disk duplicate: the cloneable-repo/local-first-memory ADR and the multi-agent-system-architecture ADR were both numbered `0024` in parallel; both keep their filenames, disambiguated as `0024-a` / `0024-b` like `0008`. Likewise `0025` ended up with two on-disk files — the offsite-backup ADR and the structured-criterion↔task-link ADR — both kept under their existing filenames and disambiguated as `0025-a` / `0025-b`; separately, the BFG ADR had also claimed `0025` on 2026-05-30 and yielded to `0026`. A third happened 2026-06-06: `0036` was claimed in parallel by the builders-subdomain ADR ([#726](https://example.com/builders#/task/726)) and this Discord-channels ADR ([#727](https://example.com/builders#/task/727)); the Discord-channels ADR yielded to `0037`. A fourth landed 2026-06-10: `0044` was claimed in parallel by the palette-replacement ADR and the per-box game-preview ADR ([#897](https://example.com/builders#/task/897)); both keep their filenames, disambiguated as `0044-a` / `0044-b` like `0008`. A fifth was caught pre-land 2026-06-13: `0050` was claimed in parallel by the device-flow-admission ADR ([#1000](https://example.com/builders#/task/1000)) and the sandbox-for-everyone ADR ([#1001](https://example.com/builders#/task/1001)); the latter yielded to `0052` before merging, so there is no `0050` duplicate. A sixth was caught pre-land 2026-06-21: the tool-agnostic-design-layer ADR ([#1375](https://example.com/builders#/task/1375)) first collided at `0079` with the value-gated-autonomous-engagement ADR, then at `0080` with the llm-cache-api-transport ADR (both part of the 0075–0080 LLM-cost-reduction series, landed on `main` in parallel); it yielded to `0081` before merging, so there is no `0079`/`0080` design-layer duplicate. A seventh landed 2026-06-23: `0084` was claimed in parallel by the auto-derived-per-task-rank-floor ADR and the remove-default-spend-cap ADR (task 1499); the rank-floor ADR landed on `main` first, so the spend-cap ADR yielded to `0085`, leaving no `0084` duplicate. An eighth landed 2026-06-27: `0095` was claimed in parallel by the borrowed-memory-and-retrieval-concepts ADR (PR #459, landed first) and the cross-agent-context-management ADR ([#1651](https://example.com/builders#/task/1651), PR #460); both keep their filenames, disambiguated as `0095-a` / `0095-b` like `0008`. These two are companions — `0095-a` adopts the memory/retrieval concepts and explicitly defers the grep-vs-semantic question to `0095-b`.)
|