@bongos/core 1.19.971 → 1.19.973

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/.bongos-core.json +116 -76
  2. package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
  3. package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
  4. package/docs/adr/0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md +1 -1
  5. package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +107 -0
  6. package/docs/adr/README.md +1 -0
  7. package/docs/api/openapi.json +1 -1
  8. package/docs/api-reference.md +1 -1
  9. package/docs/architecture.md +6 -6
  10. package/docs/branding-contract.md +1 -1
  11. package/docs/canonical-permissions.md +24 -24
  12. package/docs/copy-inventory.md +193 -187
  13. package/docs/copy-registry.json +320 -276
  14. package/docs/design/projects-hub-direction-v2.md +1 -1
  15. package/docs/design/vanilla-hall-ui-redesign-scope.md +2 -2
  16. package/docs/file-map.md +7 -7
  17. package/docs/handoff-template.md +1 -1
  18. package/docs/module-api-changelog.md +4 -0
  19. package/docs/modules-contract.md +1 -1
  20. package/docs/onboarding/drachmae-and-karma.md +2 -2
  21. package/docs/packs/engineer.md +5 -5
  22. package/docs/packs/ideator.md +1 -1
  23. package/docs/recipes/README.md +1 -1
  24. package/docs/recipes/audit-replay.md +2 -2
  25. package/docs/recipes/gds-db-backup.md +3 -3
  26. package/docs/recipes/managed-settings-remote-control.md +9 -9
  27. package/docs/recipes/memory-sync-conflicts.md +2 -2
  28. package/docs/recipes/ops-gotchas.md +24 -5
  29. package/docs/recipes/overnight-builder.md +3 -3
  30. package/docs/recipes/parallel-goal-waves.md +3 -3
  31. package/docs/recipes/reranking-a-goals-board.md +2 -2
  32. package/docs/recipes/search-index-ingestion.md +1 -1
  33. package/docs/recipes/ship-and-regrade.md +2 -2
  34. package/docs/recipes/staging-environment.md +1 -1
  35. package/docs/recipes/windows-builders.md +1 -1
  36. package/modules/hall-ui/public/deploy.html +10 -5
  37. package/modules/hall-ui/public/deploy.js +199 -110
  38. package/modules/hall-ui/public/goals-page.js +6 -0
  39. package/modules/hall-ui/public/goals-render.js +16 -0
  40. package/modules/hall-ui/public/mine-lens.js +172 -0
  41. package/modules/hall-ui/public/project-settings.css +36 -0
  42. package/modules/hall-ui/public/work-summary.js +90 -0
  43. package/modules/hall-ui/public/work.css +50 -0
  44. package/modules/hall-ui/public/work.html +24 -0
  45. package/modules/hall-ui/public/work.js +38 -51
  46. package/modules/lifecycle/db-goals.js +20 -3
  47. package/modules/lifecycle/routes/goals.js +2 -2
  48. package/modules/provisioning/core-upgrade.js +61 -2
  49. package/modules/public-landing/public/contact.html +1 -1
  50. package/modules/public-landing/public/index.html +1 -1
  51. package/modules/public-landing/public/privacy.html +1 -1
  52. package/modules/public-landing/public/projects.html +1 -1
  53. package/modules/public-landing/public/terms.html +1 -1
  54. package/package-lock.json +2 -2
  55. package/package.json +1 -1
  56. package/scripts/gds/api-alias-caller-scan.js +47 -9
  57. package/scripts/gds/cloudbongos-token-isolation.conf +33 -0
  58. package/scripts/gds/fitness.js +3 -0
  59. package/scripts/gds/provision-core-upgrade.js +132 -1
  60. package/scripts/gds/run-unit-tests.js +14 -9
  61. package/scripts/gds/scope-roster-guard.js +118 -0
  62. package/scripts/hall-preview/server.js +5 -0
  63. package/src/bongos/module-scope-map.js +1 -0
  64. package/src/module-api.js +1 -1
  65. package/tests/api_alias_caller_ratchet.mjs +68 -2
  66. package/tests/core_upgrade_door.mjs +81 -0
  67. package/tests/core_upgrade_runner.mjs +132 -0
  68. package/tests/deploy_page_projects.mjs +234 -79
  69. package/tests/fitness_scope_roster.mjs +117 -0
  70. package/tests/goal_mine_lens.mjs +233 -0
  71. package/tests/goal_tier.mjs +30 -4
  72. package/tests/hall_mine_lens.mjs +381 -0
  73. package/tests/idea_spark_hall.mjs +101 -12
  74. package/tests/known_failing_ratchet.mjs +19 -9
  75. package/tests/landing_legal_pages.mjs +5 -1
  76. package/tests/projects_hub_pre_uat.mjs +12 -6
  77. package/tests/served_paths_shipped.mjs +36 -2
@@ -1,6 +1,6 @@
1
1
  # ADR 0323 — Hosting is three shapes, and we are not the landlord
2
2
 
3
- - **Status:** accepted
3
+ - **Status:** accepted, **superseded in part by [ADR 0327](0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md)** (2026-09-22) — the platform choice, the not-the-landlord posture and decisions 2, 4, 5 and 6 STAND. Changed: §1 drops `dedicated-cloud-host` (two shapes, not three); §3's ~$14/project is the **owner's** cost, not ours, because a `cloud-host` now runs on the owner's own account; §7's capped free tier is retired with its premise — we no longer pay for customer compute.
4
4
  - **Date:** 2026-09-21
5
5
  - **Task:** [task 1004140](https://cloudbongos.com/builders#/task/1004140) (goal 1000106 — working area 1, Project creation)
6
6
  - **Deciders:** Lars (Archon) — every numbered decision below is the owner's; Claude ran the audit and the platform comparison.
@@ -1,6 +1,6 @@
1
1
  # ADR 0325 — The managed fleet control plane is the hub we already run
2
2
 
3
- - **Status:** accepted
3
+ - **Status:** accepted, **decision 4 superseded by [ADR 0327](0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md)** (2026-09-22) — decisions 1, 2, 3, 5 and 6 STAND. Decision 4's "one credential … never per-customer" is reversed: each project brings its own platform key, borrowed for standup and never stored. Its description of `/etc/cloudbongos/box.env` as "root-only on the control plane" was also wrong: the file is mode 600 owned by `lars`, not root, and the web tier runs as that same user, so the web tier could read it. **Fixed by [task 1004176](https://cloudbongos.com/builders#/task/1004176) (2026-09-23)** — a systemd drop-in carrying `InaccessiblePaths` makes the file unreadable inside the web tier's mount namespace, which is what §4 always meant even though it is not what §4 said. The runner units load it unchanged. "Root-only" remains the wrong description of the file; "unreadable by the web tier" is the right one.
4
4
  - **Date:** 2026-09-22
5
5
  - **Task:** [task 1001948](https://cloudbongos.com/builders#/task/1001948) (goal 1000106 — working area 1, Project creation)
6
6
  - **Deciders:** the owner's decisions are [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md)'s. Claude ran the inventory, and decisions 1–4 and 6 follow from that inventory plus 0323 without a new owner call — each one retires a plan rather than choosing between options. **Decision 5 is a recommendation, not a ruling**, and says so; it is filed for the owner rather than acted on.
@@ -0,0 +1,107 @@
1
+ # ADR 0327 — A cloud-host runs on the owner's own account, and the key is borrowed, not stored
2
+
3
+ - **Status:** accepted
4
+ - **Date:** 2026-09-22
5
+ - **Task:** [task 1004175](https://cloudbongos.com/builders#/task/1004175) (goal 1000106 — working area 1, Project creation)
6
+ - **Deciders:** Lars (Archon) — decisions 1, 3 and 4 are the owner's. Decision 2 is Claude's design answer to the owner's question "is there a simpler way", and decision 5 records a measurement.
7
+ - **Supersedes in part:** [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) §1 (three shapes) · §3 (who pays) · §7 (the capped trial) · [ADR 0325](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md) §4 (one credential, "never per-customer")
8
+ - **Depends on:** [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) (Render as the platform, and the not-the-landlord posture this completes)
9
+ - **Related:** [ADR 0111](0111-instance-hosting-provisioning-module.md) §2 (the trust boundary, whose enforcement §5 below corrects) · [ADR 0133](0133-github-app-manifest-onboarding-signin.md) (the short-lived-value pattern decision 2 reuses) · [ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) (the credential-free liveness pull) · [ADR 0293](0293-the-owners-deploy-door-is-an-intent-the-control-plane-drains.md) (the deploy door, which needs no platform credential)
10
+
11
+ ## Context
12
+
13
+ [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) moved a `cloud-host` off our droplet and onto Render so that we would stop being an infrastructure landlord. It left one thing unexamined: **whose Render account.** Its §3 priced a `cloud-host` at ~$14/month and its §7 capped the free tier *because we were paying*; [ADR 0325](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md) §4 then ruled the platform token is ours, "root-only on the control plane … and never per-customer".
14
+
15
+ Standing that up exposed what the unexamined assumption costs. A Render API key is issued against a *user* and carries that user's rights in **every workspace they belong to**. The owner's key reached three workspaces, one of them another builder's, holding their live production services. We would have been one bug away from acting on a third party's infrastructure with a credential they never gave us.
16
+
17
+ The same check found the second thing: **`mercury` was already on Render**, run by its own owner from their own repository. Under ADR 0323 we would have migrated a working project *off* a correct setup and onto our bill.
18
+
19
+ Both facts point the same way. The remaining landlord relationship was never the servers — it was the **account**.
20
+
21
+ ## Decision
22
+
23
+ ### 1. Each project brings its own platform key
24
+
25
+ The project's owner supplies their own Render credential. Bongos does not operate a platform account that customer projects are created inside, and does not hold a billing relationship for anyone's compute.
26
+
27
+ This retires the ~$14/project line from [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) §3 as *our* cost. It is still the owner's cost, and it is still the right number to show them — see the consequences.
28
+
29
+ It also makes the blast radius of a leaked key exactly one account: its own owner's. That property comes from this decision, not from any cryptography, and it is the strongest security property in the design.
30
+
31
+ ### 2. The key is borrowed for standup and cleared — never stored
32
+
33
+ The credential is needed **only to create the services**. Nothing afterwards needs it:
34
+
35
+ | Afterwards | How it works without a credential |
36
+ |---|---|
37
+ | Routine deploys | Render auto-deploys from Git. Render talks to GitHub; we are not in the loop. |
38
+ | The owner's deploy door ([ADR 0293](0293-the-owners-deploy-door-is-an-intent-the-control-plane-drains.md)) | Moves the core pin and pushes. Render deploys on its own. |
39
+ | Liveness and status | The [ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) pull against the instance's own public manifest. Already shipped. |
40
+ | Teardown | We disconnect. We do not delete — see decision 3. |
41
+
42
+ So the key rides the existing queue as a short-lived value: the web tier validates it, encrypts it with `src/bongos/secret-box.js` as it already exists, and the runner consumes it within one drain and **clears the column**. This is the same shape as the GitHub App Manifest handshake ([ADR 0133](0133-github-app-manifest-onboarding-signin.md)) — a value that lives in a row only long enough for the runner to spend it.
43
+
44
+ **What this deliberately does not build**, because nothing is stored: no asymmetric sealing module, no per-row credential binding, no rotation or revocation flow, and no separate-unix-user precondition. Each was designed and then deleted when the retention question was asked instead of assumed. A design that stores nothing has no storage to protect.
45
+
46
+ **What it honestly does not buy.** Encrypting the value protects it from a database dump or a nightly backup, which is the realistic threat. It does **not** defend against a compromised web tier, because the owner's key arrives there in plaintext before anything can encrypt it. That defence was never available at any retention length; only browser-side encryption would reach it, and it is not worth its cost here. Say so rather than implying the encryption is a boundary.
47
+
48
+ **The cost of not storing it:** a failed standup is retried by the owner re-entering their key, not silently by us. For a once-per-project action that is the better default — an unattended retry holding someone else's cloud credential is not a thing this platform should do.
49
+
50
+ ### 3. We never destroy a customer's infrastructure
51
+
52
+ Teardown disconnects the instance. The owner deletes their own Render services, in their own account, when they choose to stop paying.
53
+
54
+ This is why decision 2 works, but it is a safety property first: **the platform should not hold the ability to delete infrastructure it does not own.** A force-teardown that reaches into a customer's account is a capability worth not having.
55
+
56
+ ### 4. Two shapes, not three; and the free tier is free
57
+
58
+ `dedicated-cloud-host` is removed. On the owner's own account a larger plan is a setting they choose, not a hosting shape we model. [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) §1 already called it "a plan size, not a second code path" — this finishes that thought by deleting the shape as well as the code path. The shapes are:
59
+
60
+ | Shape | Meaning |
61
+ |---|---|
62
+ | `byo-host` | The owner runs the instance. |
63
+ | `cloud-host` | We stand it up, on the owner's own platform account. |
64
+
65
+ `control-plane` is unchanged and stays unrequestable. `co-tenant` stays retired and remains a valid *stored* value ([ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) §1).
66
+
67
+ **The free tier is free.** [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) §7 capped it because an indefinite subsidy at ~$14/project was not viable. We no longer pay for customer compute, so the premise is gone. Whatever caps exist are about the hub and the hall — our own costs — and are not this decision's to set.
68
+
69
+ ### 5. The ADR 0111 §2 boundary is a convention, not an operating-system boundary — recorded, not fixed here
70
+
71
+ [ADR 0111](0111-instance-hosting-provisioning-module.md) §2 says the internet-facing web tier holds no cloud token, and [ADR 0325](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md) §4 describes the token file as "root-only on the control plane". Measured on the box, 2026-09-22:
72
+
73
+ - `cloudbongos.service` runs `User=lars` with **no** sandboxing directives — no `ProtectHome`, no `ProtectSystem`, no `ReadOnlyPaths`.
74
+ - `/etc/cloudbongos/box.env` is mode `600`, owned by **`lars`**, not root.
75
+ - The runner units also run `User=lars`.
76
+
77
+ So the web tier process **can** read the DigitalOcean, Cloudflare and platform tokens. The separation is real in the *code* — the web tier only enqueues intents — but it is enforced by that code's discipline, not by the kernel. ADR 0325 §4's "root-only" is not what is on the box, and this ADR corrects it.
78
+
79
+ **Correction, 2026-09-23 (task 1004176).** This section originally carried a second finding — that `~/.config/otb/builder-secret.key` is mode **777** on a box with a second shell account. **That finding is void.** The path is a *symlink* to the real key, and `lrwxrwxrwx` is the only thing a symlink's mode ever displays on Linux; access is governed by the target, which is mode 600 owned by `lars`. The "49 bytes" was the length of the target path, not of a key. It was inferred from an `ls` listing rather than from an attempted read, which is exactly the mistake the verification note below exists to prevent. The retraction itself was then confirmed the way the original claim should have been: as the box's *other* shell account, both `cat` and a write test against that path are refused. The legacy read path in `src/instance-config.js` is real but carries no exposure.
80
+
81
+ **Resolved 2026-09-23 by [task 1004176](https://cloudbongos.com/builders#/task/1004176)**, and more narrowly than this section proposed. The remedy is not the unix-user split sketched below — that would mean relocating the checkout, the config dir, `web.env` and the secret-box master key, which the web tier *legitimately* needs for per-builder AI keys. It is a systemd drop-in carrying `InaccessiblePaths=/etc/cloudbongos/box.env`, which puts an empty mode-000 node over the file in the web tier's own mount namespace. Verified after applying: refused as the service user inside that namespace, still loaded by the runner units in the host namespace, no errors, site up. The drop-in lives at `scripts/gds/cloudbongos-token-isolation.conf`.
82
+
83
+ **A verification trap worth carrying forward:** the first check read the file as **root**, which bypasses a mode-000 over-mount via `CAP_DAC_OVERRIDE` and reported "no effect" against a working configuration. Prove an isolation as the process's own user, never as root.
84
+
85
+ **It was recorded here rather than fixed here.** It is a real pre-existing exposure of *our own* tokens, and it is emphatically **not** a precondition for decisions 1–4 — that is the point of storing nothing. Had we stored per-project keys sealed with a private key in `box.env`, this finding would have made the sealing worthless, and the two would have had to land together.
86
+
87
+ ## Consequences
88
+
89
+ - **The migration shrinks to an adoption.** `mercury` is already in the target state; its instance record needs pointing at a deployment its owner runs, not a move. <redacted> is a distinction `scripts/gds/provision-repo.js` already draws for repositories, and the platform leg needs it too. [task 1004142](https://cloudbongos.com/builders#/task/1004142) is re-scoped accordingly.
90
+
91
+ - **The fleet cost ledger's question changes, and it is no longer "what do we owe".** [ADR 0325](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md) §6 filed [task 1004148](https://cloudbongos.com/builders#/task/1004148) because `costEstimateUsd` returns 0 for a project "the platform is about to be billed for". The platform is not going to be billed for it. A ledger reporting our spend should read ~$0 per `cloud-host` and be *correct*; what an owner is worth telling is **their** estimated Render cost, which is a different column with a different meaning. Reporting one as the other is the defect now.
92
+
93
+ - **Metering and charging lose their forcing function.** [task 1001949](https://cloudbongos.com/builders#/task/1001949) was sized against recovering ~$14/project/month. There is nothing to recover. Cost *visibility* — showing an owner what their project costs them — survives as a product feature; billing for hosting does not, because we do not host.
94
+
95
+ - **`dedicated` becomes dead data as well as dead code.** Its stored value must be migrated, not merely orphaned. That migration runs over live rows and belongs with the rest of the shape rename in [task 1004142](https://cloudbongos.com/builders#/task/1004142).
96
+
97
+ - **Our own token exposure is now the largest unmitigated item in this area.** Decision 5 is filed as its own task. It was not urgent while it protected only tokens we could rotate ourselves; it stays that way, but it should not stay unowned.
98
+
99
+ ## Alternatives considered
100
+
101
+ - **Store each project's key, sealed with a key pair.** The web tier holds a public half and can only lock; the runner holds the private half and can open. Designed in full, then rejected: it buys permanent automation we found no use for, and it requires the decision-5 unix split first, because a private key in `box.env` is readable by the web tier exactly like everything else there. Three days of work and a live-site infrastructure change, to keep a value we established is needed once.
102
+
103
+ - **One platform account, ours, with every project inside it** — [ADR 0325](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md) §4 as written. Rejected: it keeps us the billing landlord, it makes one leaked credential reach every customer, and the account-wide scope of a real Render key means it would have reached workspaces we were never given.
104
+
105
+ - **Never take a key at all — generate a Blueprint and have the owner click once in Render.** Genuinely the simplest, and tempting. Rejected because the manual click is the exact friction `cloud-host` exists to remove: an owner clicking through Render's dashboard has bought very little over `byo-host`.
106
+
107
+ - **Keep the key only in the browser and have it call Render directly.** Rejected: standup is a multi-step runner job with retries and an audit trail, not a page that stays open.
@@ -418,3 +418,4 @@ This keeps the decision history honest and traceable.
418
418
  | 0324 | [**The public npm CLI channel is retired** ([task 1004144](https://cloudbongos.com/builders#/task/1004144), goal 1000091 — BONGOS-V2). The owner deleted `@cloudbongos/cli` from the registry on 2026-09-22 (`npm view` → E404; the `@cloudbongos` scope is empty). This supersedes [ADR 0258](0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md) and [ADR 0292](0292-both-install-paths-stay-and-vendoring-is-the-production-one.md) §2 — the latter titled "`@cloudbongos/cli` STAYS, on a new justification" and written *because* task 1003872 proposed removing it once the public core flip dissolved its original reason. 0292's measured argument (~0.5 MB front door vs a 46 MB core pull to print a task list) is still true and is NOT what changed; the owner's decision about carrying a public channel at all is. `scripts/gds/build-cli-package.js` + `tests/cli_package.mjs` are deleted, no doc offers `npx @cloudbongos/cli`, and the way in is the hall at `<origin>/builders` or a checkout. The COMPILED binary channel (`build-bongos.js` → `/downloads/bongos/*` via `config/bongos-cli.json`) is a different artifact and is untouched — the same near-twin task 1003889 warned about. Three tests used the package's FILES manifest as a stand-in for "what a terminal user can run" and now read `bin/bongos.js`'s verb table, which is what that set always meant.](0324-the-public-npm-cli-channel-is-retired.md) | distribution / CLI |
419
419
  | 0325 | [**The managed fleet control plane is the hub we already run** ([task 1001948](https://cloudbongos.com/builders#/task/1001948), goal 1000106 — BONGOS-V2). The spike was written against [ADR 0111](0111-instance-hosting-provisioning-module.md) §5/§7, which deferred the managed offering as **"genuinely greenfield (no fleet registry exists)"** behind two gates — Mercury and a real `bongos upgrade`. Both gates are shipped and **the premise is not**: measuring the call sites first changed the answer, as it did for [task 1004141](https://cloudbongos.com/builders#/task/1004141). The registry is `provisioning_instances` (owner, shape, domain, database, port, lifecycle, cost estimate, module selection, the liveness overlay and the served/available core-version snapshot), with `provisioning_intents` as the queue and `provisioning_events` as the audit trail; the operator is `scripts/gds/provision.js`, the single token-holder draining that queue behind ADR 0111 §2's trust boundary; oversight is `GET /provisioning/fleet` and `/fleet/cost-ledger`; and the update channel is not one mechanism but two shipped ones — the owner's per-instance deploy door ([ADR 0293](0293-the-owners-deploy-door-is-an-intent-the-control-plane-drains.md), health-gated with auto-rollback) and the opt-in patch-only sweep ([ADR 0136](0136-update-channel-subscription-policy.md)). **Decision: there is no greenfield fleet control plane to design, and none will be built — the hub IS it.** It never needed to sit *above* the per-repo databases [ADR 0100](0100-two-repo-instance-model.md) §3 locks: the roster is a hub table and the operator has never read one. The genuinely missing leg is a **platform leg** — `grep -rl RENDER_API scripts modules src` returns nothing, and after [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) a `cloud-host` is a Render service — and this ADR **deliberately specifies none of it**, because that is [task 1004142](https://cloudbongos.com/builders#/task/1004142)'s with those files open and a second spec for one implementation is how two drift. The fleet **driven** and the fleet **observed** are different populations: `byo-host` is in the registry and operated by nobody, its liveness the [ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) pull, so every operator surface must state which it counts — a roster that mixes them reports a fleet-wide upgrade as complete while an unknown number were never asked. The operator holds **exactly one** credential and it is **its own** (the platform account token, root-only on the control plane, never the web tier, never per-customer); ADR 0323 D6's "names, never values" rule is about the CUSTOMER's secrets and is unchanged. Two gaps are filed rather than fixed here: the subscription roster living in `config/update-subscriptions.json` is the one fleet fact the roster route cannot report and a restored backup can lose independently of the rows it describes ([task 1004150](https://cloudbongos.com/builders#/task/1004150), a recommendation and not a ruling), and **`costEstimateUsd` returns 0 for every shape but `dedicated`** — measured live, all 24 fleet rows report `$0`, confidently and with no error, for projects about to cost ~\$14/month each ([task 1004148](https://cloudbongos.com/builders#/task/1004148)). **No fleet-wide "upgrade everything" button**: both channels are per-instance by construction, and one action that moves the whole fleet is the shape most likely to take every customer down at once. AGPL posture unchanged — ADR 0111 §5's one-sentence rule survives its own table. Rejected: building the separate control plane §5 described (a second registry and a second operator holding the same rows with nothing keeping them equal); specifying Render here; reading per-instance databases for fleet reporting; and continuing to defer the whole layer, which is now the reason the ledger reads \$0 for a fleet about to cost real money.](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md) | hosting / fleet / provisioning |
420
420
  | 0326 | [**An upgrade may ADD a deny rule to a file the instance owns** ([task 1004156](https://cloudbongos.com/builders#/task/1004156), goal 1000090 — BONGOS-V2). Task 1003966 gave the instance starter its deny rules — no reading `.env` files, keys or session tokens, no shelving with the repo-global stash — and they reached **nobody who was already here**. The starter is written only when `.claude/settings.json` is ABSENT, and `bongos upgrade` materializes from the published package, which excludes that file on purpose; so an upgrade always takes the starter branch, always finds a settings.json already there, and left it alone. Every instance on the allow-only starter from task 1003962 — the live one included — kept it forever. That was deliberate ("rewriting a file the instance owns is the line task 1003962 drew"), and the recorded remedy was manual: re-run `bongos init`, or delete the file. **A manual remedy for a security gap protects the instances whose owners read release notes, which is not the set that needs protecting.** The owner's call: ADD the missing deny rules and NAME each one in the run output. The argument is that a deny rule is **monotonic in the safe direction** — an allow rule widens what the local agent may do, a deny rule can only narrow, so appending one cannot make any instance less safe than it was a moment earlier. That is what makes this the one merge worth doing without asking per-instance, and it extends [ADR 0121](0121-greenfield-vs-brownfield-onboarding-adopt-existing-repo.md) §2's already-licensed "merge OR leave-and-report" from `--adopt` to every materialize. "Never clobber" survives in the sense that was load-bearing: **nothing the instance wrote is removed, reordered or rewritten** — their allow rules, own denies, `ask` list, hooks, env and every key outside `permissions` come back exactly as they were, with new rules appended after. Deliberately NOT done: **no provenance**, so an owner who deletes a rule we add gets it back next upgrade and (deny beating allow in Claude Code's order) cannot override it with an allow — rejected as speculative machinery for a case nobody has hit, revisit on a real complaint; a file that will not parse, or whose `permissions`/`permissions.deny` is not the expected shape, is left BYTE-FOR-BYTE alone and reported, the posture task 1004023 took for the core's own file, because a half-understood rewrite of a permissions file is worse than the gap; and **the allow half is never topped up** — the safety argument is directionality and does not survive being applied to a rule that grants. `materializeClaude` gains `summary.settingsDenyAdded` + `settingsDenyUncheckable`, both printed through `log()` since `upgrade.js` does not iterate `summary.notes`. The test asserting an instance's settings.json comes back byte-identical is superseded by a strictly stronger one — every rule still there, unmoved, deny-only growth — the weaker spelling being what froze the gap in place. **AMENDED 2026-09-22 by [task 1004158](https://cloudbongos.com/builders#/task/1004158): the two shelf rules are now starter-only and the top-up asks for the CREDENTIAL half alone.** 0326 itself named this exclusion as the remedy if the rule ever bit; the owner took it ahead of that, because the monotonicity argument licenses a rule about reading a secret and does not reach one that guards a workflow — with no provenance and deny beating allow, an instance that loses the shelf cannot give it back. A fresh instance still gets both. The top-up only ever appends, so this is **forward-looking only**: an instance that already took them keeps them, live included.](0326-an-upgrade-may-add-a-deny-rule-to-a-file-the-instance-owns.md) | security / instance onboarding |
421
+ | 0327 | [**A `cloud-host` runs on the owner's own account, and the key is borrowed, not stored** ([task 1004175](https://cloudbongos.com/builders#/task/1004175), goal 1000106 — BONGOS-V2). [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) moved a `cloud-host` to Render to stop being a landlord but left **whose account** unexamined; [ADR 0325](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md) §4 then ruled the platform token is ours and "never per-customer". Standing it up disproved the premise: a Render API key is issued against a **user** and carries their rights in **every workspace they belong to** — the owner's reached three, one of them another builder's, holding their live production. The same check found `mercury` **already on Render**, run by its own owner, which 0323 would have migrated OFF a correct setup onto our bill. The remaining landlord relationship was never the servers; it was the **account**. **Decisions:** (1) each project brings its own platform key — we hold no billing relationship for anyone's compute, and a leaked key's blast radius is exactly its own owner's account, a property that comes from the decision and not from cryptography; (2) the key is **borrowed for standup and cleared**, never stored, because nothing afterwards needs it — Render auto-deploys from Git, the deploy door ([ADR 0293](0293-the-owners-deploy-door-is-an-intent-the-control-plane-drains.md)) is a push, liveness is the [ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) pull — so it rides the queue as a short-lived value encrypted with the existing `secret-box.js`, the [ADR 0133](0133-github-app-manifest-onboarding-signin.md) handshake's shape; (3) we **never destroy a customer's infrastructure** — teardown disconnects, the owner deletes their own services, a safety property first and the reason (2) works; (4) **two shapes, not three** (`dedicated-cloud-host` removed — on someone's own account a bigger plan is a setting, finishing 0323 §1's own "plan size, not a second code path"), and the **free tier is free**, 0323 §7's cap retired with its premise. **Deliberately NOT built, because nothing is stored:** an asymmetric sealing module, per-row credential binding, rotation/revocation, and the separate-unix-user precondition — each designed in full, then deleted when the retention question was asked instead of assumed. **Stated honestly:** encrypting the in-transit value protects it from a database dump, NOT from a compromised web tier, because the owner's key arrives there in plaintext first; that defence was never available at any retention length. **§5 records a measurement, fixed elsewhere:** [ADR 0111](0111-instance-hosting-provisioning-module.md) §2's boundary is enforced by the CODE, not the kernel — `cloudbongos.service` runs `User=lars` with no sandboxing and `/etc/cloudbongos/box.env` is mode 600 owned by `lars`, so the web tier CAN read the DigitalOcean, Cloudflare and platform tokens; 0325 §4's "root-only" is not what is on the box. (A second finding in the same section — a "mode 777" key file — was **void and is corrected in place**: the path is a symlink, whose mode display is meaningless, to a 600 target. It was inferred from an `ls` rather than an attempted read.) **Resolved 2026-09-23 by [task 1004176](https://cloudbongos.com/builders#/task/1004176)**, and more narrowly than §5 proposed — not the unix split (which would mean relocating the checkout, config dir, `web.env` and the secret-box master key the web tier legitimately needs), but a systemd drop-in with `InaccessiblePaths`, verified as the service user; proving it as **root** reports "no effect" against a working config, because `CAP_DAC_OVERRIDE` bypasses the mode-000 over-mount. It is emphatically NOT a precondition here — that is the point of storing nothing, and had we sealed per-project keys with a private key in that same file the two would have had to land together. **Consequences:** the migration shrinks to an **adoption**; the cost ledger's question inverts (our spend on a `cloud-host` is ~\$0 and correct — what an owner is worth telling is THEIR Render cost, a different column, [task 1004148](https://cloudbongos.com/builders#/task/1004148)); and metering loses its forcing function, cost *visibility* surviving as a feature while billing for hosting does not ([task 1001949](https://cloudbongos.com/builders#/task/1001949)). Rejected: storing each key sealed with a key pair (three days plus a live-site infra change, to keep a value needed once); one platform account holding every project (0325 §4 as written — keeps us the billing landlord and makes one credential reach every customer); generating a Blueprint and having the owner click once in Render (simplest, but the click is the exact friction `cloud-host` exists to remove); and keeping the key in the browser to call Render directly (standup is a runner job with retries and an audit trail, not a page that stays open).](0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md) | hosting / provisioning / credentials |
@@ -5716,7 +5716,7 @@
5716
5716
  "goals"
5717
5717
  ],
5718
5718
  "summary": "GET /goals",
5719
- "description": "GET /goals?version=&status= — list goals (read). rank: any-builder — viewing the goal board.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
5719
+ "description": "GET /goals?version=&status= — list goals (read); each row carries `viewer_is_member` for the reader. rank: any-builder — viewing the goal board.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
5720
5720
  "x-rank": "any-builder",
5721
5721
  "x-source": "modules/lifecycle/routes/goals.js",
5722
5722
  "responses": {
@@ -265,7 +265,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
265
265
 
266
266
  | Method | Path | Rank | Body | Description |
267
267
  |---|---|---|---|---|
268
- | GET | `/api/bongos/goals` | `any-builder` | — | GET /goals?version=&status= — list goals (read). |
268
+ | GET | `/api/bongos/goals` | `any-builder` | — | GET /goals?version=&status= — list goals (read); each row carries `viewer_is_member` for the reader. |
269
269
  | POST | `/api/bongos/goals` | `metic+archon` | `version_id`, `title`, `subtitle`, `description`, `scope_modules`, `succeeds_goal_id`, `category_id`, `admit_reason` | POST /goals — create a goal. |
270
270
  | GET | `/api/bongos/goals/:id` | `any-builder` | — | GET /goals/:id — one goal with its members + linked criteria (read). |
271
271
  | PATCH | `/api/bongos/goals/:id` | `any-builder` | — | PATCH /goals/:id — correct a goal's own title/subtitle/description (tasks 1003102, 1003224). |
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Split out of `CLAUDE.md` §10 in V3.R86 ([#305](https://example.com/builders#/task/305)) to keep the agent index lean. `CLAUDE.md` §10 now
4
4
  > carries a one-paragraph orientation and points here. This file is **descriptive** — for live Bongos
5
- > state (tasks, versions, claims, ranks) the source of truth is the database via `/api/gds/*`, and
5
+ > state (tasks, versions, claims, ranks) the source of truth is the database via `/api/bongos/*`, and
6
6
  > for permissions specifically see [`docs/canonical-permissions.md`](canonical-permissions.md).
7
7
 
8
8
  > **For decision rationale (why we chose what), read [`docs/adr/`](adr/). The most relevant: 0001 (stack), 0002 (DigitalOcean), 0003 (Cloudflare cert), 0006 (singleton WorldRoom + queue), 0007 (persistent identity), 0008 (Google Chat OAuth), 0010 (Bongos architecture), 0016 (trust boundary).**
@@ -217,10 +217,10 @@ audit_log(id, builder_id, route, method CHECK IN (POST|PATCH|PUT|DELETE),
217
217
 
218
218
  Bongos views: `claimable_tasks` (rebuilt in 006 to use SELECT t.*; rebuilt again in 014 so the new `discipline` column flows through), `version_progress` (rebuilt in 006 to count only leaf tasks for forward-compat with task hierarchy), `tasks_session_fit` (new in 006: derives `fits_session_modes[]` and `credits_per_minute`; rebuilt in 014 for the same SELECT t.* refresh), `task_grades_with_context` (new in 016: joins task_grades × tasks × builders × cost_log so the V4 optimizer has quality-per-dollar in one read).
219
219
 
220
- **Bongos API (`/api/gds/*`, mounted in the same Node process):**
220
+ **Bongos API (`/api/bongos/*`, mounted in the same Node process):**
221
221
 
222
222
  - **Canonical path is `/api/bongos` (task 1919); `/api/gds` is a PERMANENT alias.** The router + the discovery index are dual-mounted at both by `serve-internal.js`, driven by the single `src/bongos/api-prefix.js` constant (`API_PREFIX` + `LEGACY_API_PREFIXES`). The legacy `/api/gds` alias never goes away — shipped Dev Box binaries, the cached status mirror, and live boxes call it. **Exception:** the GitHub OAuth callback stays pinned to `/api/gds/auth/web/callback` (registered on the OAuth app; `src/bongos/routes/auth.js`). Not yet flipped (still ride the permanent alias): `src/**` internal callers + the deeper internal names (folders, `GDS_*` env, `gds_session` cookie) — the deferred internal-rename pass.
223
- - **Self-describing (task 1918, [ADR 0109](adr/0109-self-describing-openapi-and-hosted-docs.md)).** The whole surface is documented in the standard **OpenAPI 3.1** format at [`docs/api/openapi.json`](api/openapi.json), **generated from the live route files** by `scripts/gds/gen-api-docs.js` (reusing the same `route-rank-check.js` introspection the ship-time rank gate uses — so it can't drift; a fitness `--check` + ship-time regen enforce freshness). A rendered docs site (vendored Redoc) is served at **`/docs`** on the apex of every instance (`cloudbongos.com/docs` canonical); `GET /api/gds` returns a machine-discovery index pointing at it. The generated [`docs/api-reference.md`](api-reference.md) supersedes the old hand-maintained `routes-permissions.md`. The mount prefix is the single `src/bongos/api-prefix.js` constant.
223
+ - **Self-describing (task 1918, [ADR 0109](adr/0109-self-describing-openapi-and-hosted-docs.md)).** The whole surface is documented in the standard **OpenAPI 3.1** format at [`docs/api/openapi.json`](api/openapi.json), **generated from the live route files** by `scripts/gds/gen-api-docs.js` (reusing the same `route-rank-check.js` introspection the ship-time rank gate uses — so it can't drift; a fitness `--check` + ship-time regen enforce freshness). A rendered docs site (vendored Redoc) is served at **`/docs`** on the apex of every instance (`cloudbongos.com/docs` canonical); `GET /api/bongos` returns a machine-discovery index pointing at it. The generated [`docs/api-reference.md`](api-reference.md) supersedes the old hand-maintained `routes-permissions.md`. The mount prefix is the single `src/bongos/api-prefix.js` constant.
224
224
 
225
225
  - Auth: `POST /auth/device/start`, `/poll` (CLI Device Flow); `GET /auth/web/start`, `/web/callback` (browser); `POST /auth/logout`; app pairing for the Dev Box desktop app ([#904](https://example.com/builders#/task/904), [ADR 0045](adr/0045-devbox-desktop-app.md), store [`src/bongos/app-pair.js`](../src/bongos/app-pair.js) — in-memory, 10-min TTL): `POST /auth/app-pair/start` + `/poll` (public; the secret poll_token is the bearer and the minted token is delivered to it exactly once), `GET /auth/app-pair/info` + `POST /auth/app-pair/approve` (any-builder; approve is **cookie-gated** like `/auth/cli-token/issue` so a leaked bearer can't mint siblings; approving from `/builders/pair?code=…` signs the app in as the approver, `source='cli'`).
226
226
  - Own-scoped session self-service ([ADR 0206](adr/0206-own-scoped-session-revocation-no-absolute-ceiling.md), task 1003341), in [`src/bongos/routes/my-sessions.js`](../src/bongos/routes/my-sessions.js): `GET /me/sessions` (every live session on the caller's own account — source, user agent, created/last-used/expires, a `current` flag; the token value is never selected), `POST /me/sessions/revoke` (`{session_id}` — one of your own; a miss answers `revoked: 0` so a stranger's id cannot be probed) and `POST /me/sessions/revoke-all` (sign out everywhere, the requesting session included). All three gate on `requireBuilder` alone and key every statement on `req.builder.id` — an account is never named in the query, body or path. They exist because a bearer refreshes to `now()+24h` on every call, so a leaked one never expires, and the cross-builder pair (`POST /auth/revoke`, `/auth/revoke-all`, both still gated on the admin `auth.session.revoke` atom) needed an Archon the victim might not have. Surfaced in the hall at Settings → Active sessions.
@@ -232,7 +232,7 @@ Bongos views: `claimable_tasks` (rebuilt in 006 to use SELECT t.*; rebuilt again
232
232
  - Builder memory (ADR 0024 cloneable memory + ADR 0026 BFG). Self-scoped (owner is always `req.builder.id`, never input): `POST /memory/sync` (push own memory; own 64 MB parser, vs the global 64 KB), `GET /memory/files`, `GET /memory/file`, `DELETE /memory/file` (the BFG delete affordance, 6C.2). Cross-builder (input-owner) endpoints — all privileged + audit-logged fail-closed: `GET /memory/builders/:id/files`+`/file` (**archon forensic** read), `GET /memory/bfg/builders/:id/files`+`/file` (**BFG-principal only**, 6B.1 read), and the one cross-builder WRITE `POST /memory/builders/:id/bfg-write` (**BFG-principal only**, kill-switched `BFG_WRITE_ENABLED` default-OFF, namespaced `bfg/`, attributed `author: BFG`, write+audit in one tx — dreams + transparent corrections, 6C.1/6C.2).
233
233
  - Session corpus (BFG evaluator — 6D, ADR 0027): `POST /sessions` (upload own session digest; own **4 MB** json parser — capped, vs the global 64 KB — and the digest is per-turn metadata + short scrubbed snippets, never raw transcript content), `GET /sessions/mine` (self-serve), `GET /sessions/search` (cross-builder, **archon-gated** — the BFG-principal stand-in; the principal now exists (6C.1) and the *memory* cross-reads moved to it (6B.1), but these *session* reads still use the archon stand-in pending a retrofit, + audit_log row per read), `GET /sessions/:id` (own self-serve; cross-builder needs archon + audit; non-owner → 404).
234
234
  - Public (no auth, `Cache-Control: max-age=60`): `/public/versions`, `/public/progress`, `/public/cost-summary`, `/public/recent-shipped`, `/public/leaderboard`, `/public/grades[?days=N]`, `/public/tasks/:id` ([#604](https://example.com/builders#/task/604) — single-task resolver for `#NNN` doc deep-links; `title`/`value_summary`/`visual_url` returned **only** for `shipped`|`confirmed` work and withheld for in-flight tasks so unshipped/security task names can't leak; the status dashboard renders it at `#/task/:id`), `/public/ref-ids` ([#653](https://example.com/builders#/task/653) — `{tasks:[ids]}`, ids only, token-free; the doc-ref linkifier + its CI gate consult it to decide which bare `#NNN` are real task refs). These power `status.example.com`.
235
- - Live updates (Server-Sent Events — [#569](https://example.com/builders#/task/569), [ADR 0030](adr/0030-builders-hall-live-updates.md) signal layer, [ADR 0069](adr/0069-hall-live-channel-sse-decouple.md) transport): an SSE stream at `GET /api/gds/live` ([`src/bongos/routes/live.js`](../src/bongos/routes/live.js)), mounted in both entrypoints (`server.js` + `src/platform-server.js`) so the hall stays live on a game-less boot. This **superseded** the original broadcast-only Colyseus `builders_hall` room (R55 / task 1194 — the room and `src/rooms/BuildersHallRoom.js` were deleted because a Bongos-only instance has no Colyseus in the process). The change-signal layer is **unchanged**: a statement-level Postgres `NOTIFY` trigger (migration 066) on `tasks`/`claims`/`builders`/`builder_onboarding_state`/`credit_log`/`karma_log` fires a `gds_live` notification on every write (incl. direct `psql`); one persistent `LISTEN` connection ([`src/bongos/live-channel.js`](../src/bongos/live-channel.js)) relays each notification, and the SSE endpoint emits an **opaque empty nudge** to open hall tabs (the `{table, op}` is kept server-side so internal table names can't leak); the hall re-runs its `loadAll()` (debounced) on any nudge. No row data crosses the channel — access control stays on the per-panel re-fetches.
235
+ - Live updates (Server-Sent Events — [#569](https://example.com/builders#/task/569), [ADR 0030](adr/0030-builders-hall-live-updates.md) signal layer, [ADR 0069](adr/0069-hall-live-channel-sse-decouple.md) transport): an SSE stream at `GET /api/bongos/live` ([`src/bongos/routes/live.js`](../src/bongos/routes/live.js)), mounted in both entrypoints (`server.js` + `src/platform-server.js`) so the hall stays live on a game-less boot. This **superseded** the original broadcast-only Colyseus `builders_hall` room (R55 / task 1194 — the room and `src/rooms/BuildersHallRoom.js` were deleted because a Bongos-only instance has no Colyseus in the process). The change-signal layer is **unchanged**: a statement-level Postgres `NOTIFY` trigger (migration 066) on `tasks`/`claims`/`builders`/`builder_onboarding_state`/`credit_log`/`karma_log` fires a `gds_live` notification on every write (incl. direct `psql`); one persistent `LISTEN` connection ([`src/bongos/live-channel.js`](../src/bongos/live-channel.js)) relays each notification, and the SSE endpoint emits an **opaque empty nudge** to open hall tabs (the `{table, op}` is kept server-side so internal table names can't leak); the hall re-runs its `loadAll()` (debounced) on any nudge. No row data crosses the channel — access control stays on the per-panel re-fetches.
236
236
 
237
237
  **Builder ranks & the trust boundary:** moved to its own canonical reference —
238
238
  [`docs/canonical-permissions.md`](canonical-permissions.md) (rank ladder, live ranks, what's gated,
@@ -246,9 +246,9 @@ markdown describes, the DB enforces.**
246
246
 
247
247
  - **the apex root itself** — the public front door (`/`, `/projects`, `/create`, `/privacy`, `/terms`, `/contact`). **Carved to the `public-landing` module in task 1003322** ([ADR 0218](adr/0218-the-apex-root-web-surface.md)): the surface moved from repo-root `public-landing/` to `modules/public-landing/public/` and is a loader-discovered web-surface module — the THIRD, and the first that is not a subdomain. Where `hall-ui`/`status-ui` declare a Host PREFIX, this one declares `{ apexRoot: true, dir }`, because the apex is whatever host is left once the subdomains are taken (`isApexHost` = `!isStatusHost && !isBuildersHost`); `host: ""` is rejected, since `host.startsWith("")` is true for every host and would swallow the other two surfaces. Like the hall it keeps its **dedicated** serving block — in `src/platform-server.js`, now resolving its root through `moduleWebSurfaces()` by the `apexRoot` predicate rather than a hardcoded path — so the apex-only sentinel strip, the brand-token stamp and the `?v=` content stamp are unchanged. Disable the module and the front door vanishes: every route falls through to the game-less 404 tail. Served bytes are identical across the carve apart from the module's own `window.__MODULES__` entry.
248
248
  - **`example.com`** — game (Phaser + Colyseus). Static files from `public/`. `/builders` + `/builders/*` now **301-redirect to `builders.example.com/<rest>`** ([#739](https://example.com/builders#/task/739) then promoted to permanent 301 in [#741](https://example.com/builders#/task/741), ADR 0036 cutover); the apex no longer serves the builder surface directly.
249
- - **`status.example.com`** — public build dashboard. **Carved to the `status-ui` module in BV1.R84** ([ADR 0093](adr/0093-tranche-2-core-carve-sequence.md) §1): the surface moved from `public-status/` to `modules/status-ui/public/`, now served by the loader's web-surface seam (`moduleWebSurfaces` → `serve-internal.js`, routed on the `status.` Host prefix the manifest declares) instead of a hardcoded block — no behavior change. Layout reorganized in [#726](https://example.com/builders#/task/726) (ADR 0036): Pulse / Watch(uptime) / Architects / Works / Treasury / Lately Inscribed / The World — the grade-aggregate, estimation-drift, and repo-health tablets moved to the Archon `/watch` page. **⚠ Served OFF the droplet** (ADR 0029, outage resilience): the live page is **GitHub Pages from the separate repo `example-owner/example-status`** (CNAME → Fastly), reading data cross-origin from the droplet's `/api/gds/public/*` (CORS open) and an availability banner from the prober's `status-data` branch. The droplet still serves `modules/status-ui/public/` (on the `status.` host) as a same-origin fallback, but **DNS points at Pages, not the droplet** — so a droplet deploy does NOT update the live page. Changes to the surface publish to the mirror via `scripts/status-mirror-sync.js` (re-pointed at `modules/status-ui/public/`; status.js/style.css/cursors are byte-identical copies; index.html gets the off-box bits injected), run automatically by `.github/workflows/sync-status-mirror.yml` (needs the `STATUS_MIRROR_TOKEN` secret) ([#737](https://example.com/builders#/task/737)). **⚠ FOLLOW-UP (R84):** that workflow's path trigger still watches `public-status/**` and must be re-pointed at `modules/status-ui/public/**` by an owner/Archon — the GitHub App can't push `.github/workflows/*` (learning 107), so until then the mirror sync won't auto-fire on future status edits.
249
+ - **`status.example.com`** — public build dashboard. **Carved to the `status-ui` module in BV1.R84** ([ADR 0093](adr/0093-tranche-2-core-carve-sequence.md) §1): the surface moved from `public-status/` to `modules/status-ui/public/`, now served by the loader's web-surface seam (`moduleWebSurfaces` → `serve-internal.js`, routed on the `status.` Host prefix the manifest declares) instead of a hardcoded block — no behavior change. Layout reorganized in [#726](https://example.com/builders#/task/726) (ADR 0036): Pulse / Watch(uptime) / Architects / Works / Treasury / Lately Inscribed / The World — the grade-aggregate, estimation-drift, and repo-health tablets moved to the Archon `/watch` page. **⚠ Served OFF the droplet** (ADR 0029, outage resilience): the live page is **GitHub Pages from the separate repo `example-owner/example-status`** (CNAME → Fastly), reading data cross-origin from the droplet's `/api/bongos/public/*` (CORS open) and an availability banner from the prober's `status-data` branch. The droplet still serves `modules/status-ui/public/` (on the `status.` host) as a same-origin fallback, but **DNS points at Pages, not the droplet** — so a droplet deploy does NOT update the live page. Changes to the surface publish to the mirror via `scripts/status-mirror-sync.js` (re-pointed at `modules/status-ui/public/`; status.js/style.css/cursors are byte-identical copies; index.html gets the off-box bits injected), run automatically by `.github/workflows/sync-status-mirror.yml` (needs the `STATUS_MIRROR_TOKEN` secret) ([#737](https://example.com/builders#/task/737)). **⚠ FOLLOW-UP (R84):** that workflow's path trigger still watches `public-status/**` and must be re-pointed at `modules/status-ui/public/**` by an owner/Archon — the GitHub App can't push `.github/workflows/*` (learning 107), so until then the mirror sync won't auto-fire on future status edits.
250
250
  - **`builders.example.com`** — dedicated builder surface (`isBuildersHost`, ADR 0036). **Carved to the `hall-ui` module in BV1.R85** ([ADR 0093](adr/0093-tranche-2-core-carve-sequence.md) §1): the surface moved from `public-builders/` to `modules/hall-ui/public/` (~13k LOC, the largest module) and is a loader-discovered web-surface module (`contributes.webSurfaces` host `builders.`). UNLIKE the generic status surface it keeps its **dedicated** serving block in `serve-internal.js` (`BUILDERS_DIR = modules/hall-ui/public/`), partitioned out of the generic web-surface loop on the `isBuildersHost` predicate. A thin rewrite shim maps every non-API path onto the existing `/builders/*` tree (so all serving + gating + asset-stamping is reused), serving the hall (`/`), the Metic+ **`/watch`** monitoring page (gated by `requireGovernmentPage`), **`/settings`** (Voices + Craft prefs), plus ranks/sessions/primer/diagrams. No behavior change — byte-identical (only `window.__MODULES__.enabled` gains `hall-ui:true`). **Additive** — until its DNS A record + cert are live, the whole surface stays reachable at `example.com/builders/*`. No `.github/workflows/*` follow-up: unlike R84, no workflow path-trigger watches `public-builders/**`.
251
- - The `/api/gds/*` surface is reachable from every host. **Cross-subdomain sign-in:** the `gds_session` cookie is scoped to `.example.com` on the production hosts (apex/www/builders/status) and host-only on localhost/staging (no cross-env token bleed); the OAuth handshake runs on the canonical origin with a validated `?return=` (open-redirect-guarded) so the subdomain can land a builder back on itself.
251
+ - The `/api/bongos/*` surface is reachable from every host. **Cross-subdomain sign-in:** the `gds_session` cookie is scoped to `.example.com` on the production hosts (apex/www/builders/status) and host-only on localhost/staging (no cross-env token bleed); the OAuth handshake runs on the canonical origin with a validated `?return=` (open-redirect-guarded) so the subdomain can land a builder back on itself.
252
252
  - Caddy gets a site block per host (`infra/Caddyfile` (instance-side)), all reverse-proxying to `127.0.0.1:3000`.
253
253
 
254
254
  **Asset cache-busting (content-hash `?v=`, task [#521](https://example.com/builders#/task/521)).** The hall + status HTML pages (`modules/hall-ui/public/*.html`, `modules/status-ui/public/index.html`) are served from an **in-memory stamped copy**, not straight off disk: at boot [`src/asset-version.js`](../src/asset-version.js) rewrites each local css/js ref's `?v=` query to a 12-hex hash of the referenced file's content (`buildStampedHtml`). The on-disk HTML keeps a hand-typed `?v=` only as a fallback. This is the durable fix for stale-asset serving — Cloudflare overrides the origin's `max-age=60` with a **4h browser TTL** for js/css, so before this, editing `builders.js` without manually bumping its `?v=` left every returning browser (and CF's edge) serving pre-deploy bytes for hours. Now any content change → new hash → new URL → fetched fresh automatically. HTML pages stay short-cached (CF serves them DYNAMIC), so the new `?v=` reaches clients within ~60s. Assets and non-HTML paths still fall through to `express.static`; the sign-in gate on primer/diagrams runs before the stamped serving.
@@ -61,7 +61,7 @@ Tests drive the pure resolver with injected objects (no I/O): `resolveBranding({
61
61
  The browser never sees the full pack — only the **client-safe projection** `clientBranding()` (a small allow-list — `{ identity, currency, theme, copy, rankLabels }`; origins, repo, db name, `llmProjectName`, and env prefix stay server-side). It reaches the front-end two ways:
62
62
 
63
63
  - **On-box (hall + status served from the droplet):** injected synchronously by `mountInternalSurfaces` (`src/bongos/serve-internal.js` → `injectBrandingGlobal`) into the page `<head>` as **`window.__BRANDING__`** (the strings) **plus theme markup** (`themeMarkup` → a `:root` palette/font-var override `<style>`, the web-font `<link>`s, and the favicon `<link>`). The `<style>` lands after the page's own stylesheet so the pack wins. No fetch, no flash-of-default.
64
- - **Off-box (the GitHub-Pages status page):** read cross-origin from **`GET /api/gds/public/branding`**.
64
+ - **Off-box (the GitHub-Pages status page):** read cross-origin from **`GET /api/bongos/public/branding`**.
65
65
 
66
66
  R56 seeded the projection with the configurable **currency label** (`public-builders/brand-apply.js` fills `[data-currency-label]` / `[data-currency-tmpl]` markup + `window.OTBBranding.currencyLabel()`). R57 widens it to the **LOOK** — `theme.ui` (palette + fonts + favicon; note this is the WEB theme, distinct from the server-only `theme.palette` game pixel-art path) — and the **rank display-label map** (`window.OTB.rankLabel()` reads `rankLabels`; the rank ENUM stays Greek by decision, only the label is presentation). The remaining themed **page copy + identity strings** (titles, ledes, world-flavor copy) are tracked as the R57 copy-pack follow-up.
67
67
 
@@ -23,7 +23,7 @@ de-facto permission *state*. `CLAUDE.md` now points here.
23
23
  |---|---|---|
24
24
  | `builders.rank` (prod DB, migration 023) | **the** source of authority | ✅ yes — the boundary itself |
25
25
  | `requireRank()` middleware on each route (`src/bongos/auth.js`) | enforcement point | ✅ yes |
26
- | `PATCH /api/gds/builders/:id/rank` (Archon-only, task [#232](https://example.com/builders#/task/232) / V3.R07) | the *only* writer of rank | ✅ yes |
26
+ | `PATCH /api/bongos/builders/:id/rank` (Archon-only, task [#232](https://example.com/builders#/task/232) / V3.R07) | the *only* writer of rank | ✅ yes |
27
27
  | `CLAUDE.md`, this file, ADRs | **describe** the model for humans/agents | ❌ no — documentation only |
28
28
  | `.claude/skills/*`, `.claude/settings.json`, hooks | call server endpoints; cannot grant authority | ❌ no |
29
29
  | session token / cookie | identity, not authority (carries no rank) | ❌ no |
@@ -137,11 +137,11 @@ or missing ranks fail closed.
137
137
  - **Archon-only** — **rank, identity, and the four escalation keys ([ADR 0157](adr/0157-archon-is-rank-and-identity-only.md), owner decision 2026-08-05).**
138
138
  That ADR narrowed Archon from ~50 route gates to 9 live ones, on the rule *"the Archon rank is
139
139
  responsible for rank and identity only"*:
140
- - **rank** — `PATCH /api/gds/builders/:id/rank` (the single writer).
141
- - **identity** — `PATCH /api/gds/builders/:id/status` (offboard/(de)activate), `GET|PATCH /api/gds/access-requests[/:id]`
140
+ - **rank** — `PATCH /api/bongos/builders/:id/rank` (the single writer).
141
+ - **identity** — `PATCH /api/bongos/builders/:id/status` (offboard/(de)activate), `GET|PATCH /api/bongos/access-requests[/:id]`
142
142
  (who is admitted), `POST /api/gds/auth/revoke[-all]` (whose sessions are valid).
143
- - **the four escalation keys**, which look operational but are not: `POST /api/gds/tasks/:id/confirm`
144
- (grader-bypass), `POST /api/gds/gate-approvals/:pr/approve` (CI deploy-gate override), protected-module
143
+ - **the four escalation keys**, which look operational but are not: `POST /api/bongos/tasks/:id/confirm`
144
+ (grader-bypass), `POST /api/bongos/gate-approvals/:pr/approve` (CI deploy-gate override), protected-module
145
145
  **goal scope** ([ADR 0086](adr/0086-goal-scoped-work-hierarchy.md)), and `government.manage` (the
146
146
  ranks/permissions tab). Delegating any one of them lets a Metic reach Archon in four steps — create a
147
147
  protected-scope goal in the permission core, claim a task there (protected paths already floor at
@@ -149,7 +149,7 @@ or missing ranks fail closed.
149
149
  past the failed grade, approve their own red gate — so keeping rank Archon-only depends on keeping
150
150
  these Archon-only too. **Any future delegation must re-check that chain.**
151
151
  - plus `version.close` (paired with rank as the powers that never delegate), the cross-tier
152
- `POST|DELETE /api/gds/dependencies` (`system: true`), and the substrate floors
152
+ `POST|DELETE /api/bongos/dependencies` (`system: true`), and the substrate floors
153
153
  (`deploy.push_main` / `path.protected.modify` / `infra.ssh`, ADR 0043).
154
154
 
155
155
  > **Amended by [ADR 0172](adr/0172-editable-rank-roles-substrate-axis.md) (owner decision, 2026-08-16).**
@@ -166,14 +166,14 @@ or missing ranks fail closed.
166
166
  > so no amount of in-app escalation produces a push to `main`, a protected-path edit, or an ssh session.
167
167
  > `builders.rank` remains the only writer of who holds a rank (R95b).
168
168
 
169
- Earlier one-power delegations that ADR 0157 generalises: **task authoring** — `POST /api/gds/tasks`,
170
- `PATCH /api/gds/tasks/:id` (incl. the `newcomer_friendly` flag), `POST /api/gds/tasks/:id/promote`, and the
169
+ Earlier one-power delegations that ADR 0157 generalises: **task authoring** — `POST /api/bongos/tasks`,
170
+ `PATCH /api/bongos/tasks/:id` (incl. the `newcomer_friendly` flag), `POST /api/bongos/tasks/:id/promote`, and the
171
171
  `tasks/:id/{dependencies,criteria}` CRUD — moved to Metic+ in [ADR 0090](adr/0090-metic-task-authoring.md),
172
- and **task retirement** — `POST /api/gds/tasks/:id/abandon`, plus the `abandoned → backlog` restore on
172
+ and **task retirement** — `POST /api/bongos/tasks/:id/abandon`, plus the `abandoned → backlog` restore on
173
173
  `/promote` — in [ADR 0152](adr/0152-metic-task-abandonment.md) (see the Metic+Archon bullet).
174
174
 
175
175
  > **The ownership axis: a rule about WHOSE row, which no rank can express** ([ADR 0297](adr/0297-a-tasks-creator-may-retire-it.md),
176
- > owner request 2026-07-25). `POST /api/gds/tasks/:id/abandon` is the first route decided on the
176
+ > owner request 2026-07-25). `POST /api/bongos/tasks/:id/abandon` is the first route decided on the
177
177
  > [ADR 0151](adr/0151-governance-permissions-as-atom-ranks-as-roles.md) §3 ownership axis, and so the first whose
178
178
  > middleware list is deliberately **not** the whole gate. Two authorities reach it:
179
179
  >
@@ -198,20 +198,20 @@ or missing ranks fail closed.
198
198
  > are untouched for both paths — never a shipped task, never one with an active claim. And a task with
199
199
  > `created_by IS NULL` (everything filed before `core_227`, plus the seed/restock/Discord vectors)
200
200
  > belongs to **nobody**: the control never appears on it, and no ownership grant can reach it.
201
- `POST /api/gds/done-when/:id/unsatisfy` **was** Archon-only and moved to Metic in ADR 0157, superseding
201
+ `POST /api/bongos/done-when/:id/unsatisfy` **was** Archon-only and moved to Metic in ADR 0157, superseding
202
202
  [ADR 0154](adr/0154-goal-owner-criterion-authoring.md)'s "the backward ratchet stays Archon" carve-out;
203
203
  satisfy and unsatisfy remain separate permissions, so confirming a criterion still never confers
204
204
  un-confirming one. **A manual promotion advances exactly
205
205
  ONE live rung at a time** (`xenos→thetes→metic→archon`; [#707](https://example.com/builders#/task/707)) — a multi-rung jump (e.g. `xenos→metic`) returns
206
206
  `400 multi_rung_promotion` naming the next allowed rung. Demotions are unconstrained (offboarding
207
207
  drops straight to `xenos`); the automatic `xenos→thetes` graduation is +1 and unaffected.
208
- - **Metic + Archon** — the working rank's review/triage powers ([#360](https://example.com/builders#/task/360) / ADR 0018): `PATCH /api/gds/inbox/:id`
209
- (idea triage), `POST /api/gds/blockers/:id/{resolve,link}` (blocker review) — **plus, since
210
- [ADR 0090](adr/0090-metic-task-authoring.md), full task authoring**: `POST /api/gds/tasks`,
211
- `PATCH /api/gds/tasks/:id`, `POST /api/gds/tasks/:id/promote`, the `tasks/:id/dependencies` +
212
- `tasks/:id/criteria` CRUD, and `POST /api/gds/done-when/:id/satisfy` (criterion confirm,
208
+ - **Metic + Archon** — the working rank's review/triage powers ([#360](https://example.com/builders#/task/360) / ADR 0018): `PATCH /api/bongos/inbox/:id`
209
+ (idea triage), `POST /api/bongos/blockers/:id/{resolve,link}` (blocker review) — **plus, since
210
+ [ADR 0090](adr/0090-metic-task-authoring.md), full task authoring**: `POST /api/bongos/tasks`,
211
+ `PATCH /api/bongos/tasks/:id`, `POST /api/bongos/tasks/:id/promote`, the `tasks/:id/dependencies` +
212
+ `tasks/:id/criteria` CRUD, and `POST /api/bongos/done-when/:id/satisfy` (criterion confirm,
213
213
  [ADR 0086](adr/0086-goal-scoped-work-hierarchy.md)). Since
214
- [ADR 0154](adr/0154-goal-owner-criterion-authoring.md), also `POST /api/gds/versions/:id/done-when`
214
+ [ADR 0154](adr/0154-goal-owner-criterion-authoring.md), also `POST /api/bongos/versions/:id/done-when`
215
215
  (criterion **create**) — but walled by goal ownership, not by rank alone: a non-Archon must name a
216
216
  `goal_id`, on an open (`planning`/`building`) version, in an `open` goal they **own or manage** (the same
217
217
  `authorizeGoalManagement` wall as goal visibility/invites/memberships — the membership column is
@@ -224,13 +224,13 @@ or missing ranks fail closed.
224
224
  **[ADR 0157](adr/0157-archon-is-rank-and-identity-only.md) added the whole operational block here** —
225
225
  20 permissions that had been Archon-gated only by ADR 0016's fail-closed default, never by a decision:
226
226
  the government/monitoring **pages** (`/watch`, `/harbor`, `/gate` — `page.view.government`) and
227
- `GET /api/gds/sessions/search` (whose own `/sessions` page had no server gate at all until
227
+ `GET /api/bongos/sessions/search` (whose own `/sessions` page had no server gate at all until
228
228
  task 1002710 put it behind `page.view.government` alongside its three group-mates); the
229
229
  **dev-box + provisioning fleets** (`GET /boxes[/cost-ledger]`,
230
230
  `POST /boxes/:b/close`, `PATCH .../block`, `GET /provisioning/fleet[/cost-ledger]`,
231
231
  `POST .../force-teardown`); **security adjudication** (`GET /security/reports`,
232
232
  `POST /security/reports/:id/{confirm,dispute,fix,link-fix}`, `/security/docs` CRUD);
233
- `GET /api/gds/audit-log`; the **roster reads** (`GET /builders/roster`, `/grades/by-builder`,
233
+ `GET /api/bongos/audit-log`; the **roster reads** (`GET /builders/roster`, `/grades/by-builder`,
234
234
  `/builders/:id/active-claims`); **cross-builder memory reads** (`GET /memory/builders/:id/file[s]`);
235
235
  **project curation + scouting**; `PATCH /builders/:id/budget`; `POST /inbox/:id/ratify`;
236
236
  `GET /override-requests` + `POST .../:id/decide`; `POST /claims/:id/release-on-behalf`;
@@ -251,7 +251,7 @@ or missing ranks fail closed.
251
251
  the builder's tier is below it).
252
252
  - **Every-privileged-route audit:** task [#231](https://example.com/builders#/task/231) (V3.R06) applies `requireRank()` to *every* privileged
253
253
  route so none is left ungated; the periodic auditor ([#266](https://example.com/builders#/task/266)/[#267](https://example.com/builders#/task/267)) asserts the boundary stays intact.
254
- - **Rank writer:** `PATCH /api/gds/builders/:id/rank` (task [#232](https://example.com/builders#/task/232) / V3.R07) — Archon-only, the single
254
+ - **Rank writer:** `PATCH /api/bongos/builders/:id/rank` (task [#232](https://example.com/builders#/task/232) / V3.R07) — Archon-only, the single
255
255
  writer of rank.
256
256
 
257
257
  **Default posture for any new write/privileged route: `requireRank('archon')`** until a lower rank is
@@ -269,8 +269,8 @@ nothing set it, so every task sat at the `'xenos'` default and the claim gate ab
269
269
  floor and the [ADR 0043](adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md) git-push floor can never disagree). Everything else stays `'xenos'` (the open
270
270
  general queue). The rule **never auto-assigns `archon`** — owner-only is a deliberate manual call.
271
271
  - **Override is upward-only (`db.highestRank`):** the stored value is `max(derived floor, requested)`.
272
- An Archon may RAISE a task's floor (incl. to `archon`) on `POST /api/gds/tasks` or
273
- `PATCH /api/gds/tasks/:id` (`requires_rank`, Archon-only); a request **below** the derived floor is
272
+ An Archon may RAISE a task's floor (incl. to `archon`) on `POST /api/bongos/tasks` or
273
+ `PATCH /api/bongos/tasks/:id` (`requires_rank`, Archon-only); a request **below** the derived floor is
274
274
  clamped up (fail-closed). Flipping `security_sensitive` on ratchets the floor up; it never lowers one.
275
275
  - **Backfill:** migration `<redacted>` raised every non-terminal task that was
276
276
  still at the `'xenos'` default to `metic` where sensitive (raise-only, idempotent).
@@ -284,7 +284,7 @@ Authority over *the code itself* rides the same ladder, enforced server-side fro
284
284
  - **Local clone onto your own disk = Metic and up.** Below Metic (`xenos`, `thetes`), there is **no
285
285
  permanent local copy** — the collaborator-invite path is a Metic+ affordance. New builders work
286
286
  inside the org-controlled cloud box, where access is revocable and audited.
287
- - **Box source is scoped by rank** (`boxScopeForRank`, decided live inside `GET /api/gds/box/source-access`):
287
+ - **Box source is scoped by rank** (`boxScopeForRank`, decided live inside `GET /api/bongos/box/source-access`):
288
288
  `xenos`/`thetes` get a sparse `starter` surface; `metic`/`archon` get `full`. A demotion cuts a box
289
289
  off on its very next fetch (instant credential-layer revocation; the droplet teardown is the slower
290
290
  control-plane half via `box.js reconcile`).
@@ -295,7 +295,7 @@ Authority over *the code itself* rides the same ladder, enforced server-side fro
295
295
 
296
296
  ### Git and SSH are OUTSIDE the HTTP boundary — covered by a separate rank floor (ADR 0043, SEC [#863](https://example.com/builders#/task/863))
297
297
 
298
- ADR 0016's per-request rank check covers `/api/gds/*` only. Two surfaces it does **not** cover —
298
+ ADR 0016's per-request rank check covers `/api/bongos/*` only. Two surfaces it does **not** cover —
299
299
  `git push` to `main` and `ssh` to the droplet — were used in the Builder 25 incident to land an
300
300
  un-graded tiering change straight on prod `main`. **Since 2026-08-08 (task 1002619), `main` HAS
301
301
  live branch protection: the required `unit` status check blocks every direct push, so all work