@bongos/core 1.19.870 → 1.19.872
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 +164 -94
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/CONTRIBUTING.md +1 -1
- package/README.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +10 -0
- package/docs/api/openapi.json +61 -3
- package/docs/api-reference.md +7 -1
- package/docs/architecture.md +7 -7
- package/docs/branding-contract.md +1 -1
- package/docs/copy-inventory.md +190 -189
- package/docs/copy-registry.json +264 -255
- package/docs/design/hall-direction-v2.md +2 -2
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/diagrams/README.md +4 -4
- package/docs/onboarding/drachmae-and-karma.md +2 -2
- package/docs/onboarding/primer.md +1 -1
- package/docs/recipes/README.md +1 -1
- package/docs/recipes/bongos-cli-release.md +4 -4
- package/docs/recipes/core-release-pipeline.md +3 -3
- package/docs/recipes/instance-deploy.md +2 -2
- package/docs/recipes/ops-gotchas.md +2 -2
- package/docs/recipes/search-index-ingestion.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/gauge.js +218 -0
- package/modules/dev-box/app/README.md +1 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
- package/modules/hall-ui/public/blockers.js +138 -34
- package/modules/hall-ui/public/builders.js +2 -1
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/collab.js +6 -3
- package/modules/hall-ui/public/dom-utils.js +46 -5
- package/modules/hall-ui/public/goals-page.js +7 -3
- package/modules/hall-ui/public/goals-render.js +4 -3
- package/modules/hall-ui/public/hall-render.js +1 -1
- package/modules/hall-ui/public/people.js +3 -3
- package/modules/hall-ui/public/profile-charts.js +5 -2
- package/modules/hall-ui/public/profile.html +3 -3
- package/modules/hall-ui/public/profile.js +6 -6
- package/modules/hall-ui/public/settings-specialities.js +2 -2
- package/modules/hall-ui/public/settings.html +4 -4
- package/modules/hall-ui/public/settings.js +2 -2
- package/modules/hall-ui/public/shell.js +70 -0
- package/modules/hall-ui/public/style.css +31 -0
- package/modules/hall-ui/public/work.js +12 -14
- package/modules/provisioning/module.json +3 -3
- package/modules/public-landing/public/terms.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/autobongos-loop.js +170 -0
- package/scripts/gds/autobongos-run.js +317 -0
- package/scripts/gds/autobongos-verify.js +377 -0
- package/scripts/gds/autonomy-gauge-statusline.js +124 -0
- package/scripts/gds/autonomy-gauge.js +84 -0
- package/scripts/gds/run-unit-tests.js +120 -2
- package/scripts/gds/sequence.js +3 -0
- package/scripts/gds/start.js +16 -10
- package/src/bongos/core-update.js +147 -0
- package/src/bongos/routes/core-update.js +49 -0
- package/src/bongos/routes.js +2 -0
- package/src/module-api.js +1 -1
- package/tests/account_privacy_flags.mjs +3 -1
- package/tests/autobongos_loop.mjs +446 -0
- package/tests/autobongos_verify.mjs +325 -0
- package/tests/autonomy_gauge.mjs +344 -0
- package/tests/blockers_hall.mjs +20 -6
- package/tests/blockers_kit_pager.mjs +260 -0
- package/tests/blockers_page_ui.mjs +5 -4
- package/tests/box_ship_permissions.mjs +30 -0
- package/tests/core_update_banner.mjs +308 -0
- package/tests/eviction_policy.mjs +22 -1
- package/tests/git_guard.mjs +68 -0
- package/tests/goal_criterion_confirm_ui.mjs +7 -0
- package/tests/goal_invite_consent.mjs +7 -0
- package/tests/goal_sort_staleness.mjs +6 -2
- package/tests/goal_subtitle.mjs +48 -4
- package/tests/hall_board_world.mjs +4 -1
- package/tests/hall_builders_page_pass.mjs +7 -0
- package/tests/hall_landing_boot.mjs +7 -0
- package/tests/hall_profile_world.mjs +20 -2
- package/tests/hall_record_world.mjs +7 -0
- package/tests/hall_roster_world.mjs +2 -1
- package/tests/hall_studio_world.mjs +7 -0
- package/tests/hook_shell_tools.mjs +23 -1
- package/tests/idea_detail_page.mjs +7 -0
- package/tests/idea_spark_hall.mjs +7 -0
- package/tests/integration_parked_ratchet.mjs +79 -0
- package/tests/module_contributions.mjs +134 -8
- package/tests/people_directory_clip.mjs +7 -0
- package/tests/profile_activity.mjs +7 -0
- package/tests/profile_ui_cross_project.mjs +9 -1
- package/tests/provision_disconnect.mjs +7 -0
- package/tests/provision_restart.mjs +7 -0
- package/tests/provision_settings_apply.mjs +7 -0
- package/tests/sessions_kit_pager.mjs +7 -0
- package/tests/watch_applications_queue.mjs +7 -0
- package/tests/watch_roster_self_row.mjs +7 -0
- package/tests/watch_sealed_floor.mjs +7 -0
|
@@ -118,7 +118,7 @@ This mirrors what `ship.js` does automatically; it's here because /merge-mode de
|
|
|
118
118
|
|
|
119
119
|
**Step 5 — smoke tests, push, deploy.**
|
|
120
120
|
|
|
121
|
-
> **Deploy mode (#679 / [ADR 0042](../../../docs/adr/0042-builder-self-deploy-ci-auto-merge.md)).** `config/deploy.json` is now `mode: ci` (CI cutover landed 2026-06-13), so the **`ci`** path is current: there is no laptop merge and no SSH at all — landing happens via PR auto-merge and
|
|
121
|
+
> **Deploy mode (#679 / [ADR 0042](../../../docs/adr/0042-builder-self-deploy-ci-auto-merge.md)).** `config/deploy.json` is now `mode: ci` (CI cutover landed 2026-06-13), so the **`ci`** path is current: there is no laptop merge and no SSH at all — landing happens via PR auto-merge and `.github/workflows/deploy-prod.yml` (instance-side) deploys `main`→prod from Actions. In `ci` mode, do NOT run the `git push origin main` + `ssh … ~/deploy.sh` lines below — instead confirm the PR auto-merged and the `deploy-prod` Actions run went green. The block below is the **legacy `laptop`** deploy mode (the operator's machine pushes `main` and SSHes the droplet) — it is the fallback path and only applies if `config/deploy.json` is reverted to `mode: laptop`. Check the active mode with `node scripts/gds/ship.js --print-deploy-mode`.
|
|
122
122
|
|
|
123
123
|
> **Do NOT probe `parseDeployMode` directly (task 1003221).** It reads only the env var and `config/deploy.json`, and **defaults to `laptop`** when neither is present — so on any machine whose checkout does not carry `config/`, that probe answers `laptop` every time. It sent a builder down the laptop merge path — local merge, `push-main.js`, SSH deploy — on a machine with none of those credentials (2026-08-21, 163 minutes). `effectiveDeployMode` is the credential-aware answer and reroutes to `ci` when the machine cannot push locally; the flag above prints that one.
|
|
124
124
|
|
package/CONTRIBUTING.md
CHANGED
|
@@ -44,7 +44,7 @@ Permissions are **server-enforced on the live database, per request, with no cac
|
|
|
44
44
|
|
|
45
45
|
## Reporting security issues
|
|
46
46
|
|
|
47
|
-
Do **not** open a public issue for a vulnerability. Security reporting and the bounty process are handled through the platform's security surface (
|
|
47
|
+
Do **not** open a public issue for a vulnerability. Security reporting and the bounty process are handled through the platform's security surface (`/builder-redteam` files a report for the owner). Responsible disclosure is rewarded.
|
|
48
48
|
|
|
49
49
|
## Code of conduct
|
|
50
50
|
|
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ We keep secrets (`.env` files, API keys, private keys) out of the repo with two
|
|
|
68
68
|
|
|
69
69
|
1. **Read the output.** The hook prints the offending file(s) and why.
|
|
70
70
|
2. **Is it a real secret?** Then it must never be committed. Remove it from the commit (`git restore --staged <file>`), and move the value out of the repo — local secrets go in `.env.local` (gitignored; run `/builder-setup`); production secrets live in your instance's secret store, never in the tree. If it was *already committed in an earlier commit*, the history scan will keep failing until the history is scrubbed — flag this to the owner, since rewriting shared history needs care.
|
|
71
|
-
3. **Is it a false positive?** (e.g. an example key in docs, a test fixture, a Stripe `pk_test_` publishable key.) Add a path regex — one per line — to
|
|
71
|
+
3. **Is it a false positive?** (e.g. an example key in docs, a test fixture, a Stripe `pk_test_` publishable key.) Add a path regex — one per line — to `.husky/secrets-allowlist.txt` (created on first use). Both the hook and CI read that allowlist. Keep entries narrow and only add them after confirming the match is genuinely not a secret.
|
|
72
72
|
4. **Genuine emergency commit** (you're sure it's clean and need to move now): `git commit --no-verify` skips the local hook — but CI will still scan the PR, so this only defers the check, it doesn't bypass it.
|
|
73
73
|
|
|
74
74
|
## If your CLI starts 401'ing
|
|
@@ -5,7 +5,7 @@ A **generated**, zero-dependency typed client for the Bongos API — produced fr
|
|
|
5
5
|
by hand; it regenerates when the spec changes, so it can never drift from the routes.
|
|
6
6
|
|
|
7
7
|
- API version: **v1** (served at `/api/bongos/v1`)
|
|
8
|
-
-
|
|
8
|
+
- 394 operations across 60 resource groups
|
|
9
9
|
|
|
10
10
|
## Use it from your project
|
|
11
11
|
|
|
@@ -324,6 +324,10 @@ function createClient(opts = {}) {
|
|
|
324
324
|
// GET /copy-desk/strings — rank: any-builder — GET /copy-desk/strings
|
|
325
325
|
getCopyDeskStrings: (args) => request("GET", "/copy-desk/strings", { hasBody: false }, args),
|
|
326
326
|
},
|
|
327
|
+
"coreUpdate": {
|
|
328
|
+
// GET /core-update — rank: archon — GET /core-update
|
|
329
|
+
getCoreUpdate: (args) => request("GET", "/core-update", { hasBody: false }, args),
|
|
330
|
+
},
|
|
327
331
|
"cost": {
|
|
328
332
|
// POST /cost — rank: any-builder — POST /cost
|
|
329
333
|
postCost: (args) => request("POST", "/cost", { hasBody: true }, args),
|
|
@@ -323,6 +323,10 @@ function createClient(opts = {}) {
|
|
|
323
323
|
// GET /copy-desk/strings — rank: any-builder — GET /copy-desk/strings
|
|
324
324
|
getCopyDeskStrings: (args) => request("GET", "/copy-desk/strings", { hasBody: false }, args),
|
|
325
325
|
},
|
|
326
|
+
"coreUpdate": {
|
|
327
|
+
// GET /core-update — rank: archon — GET /core-update
|
|
328
|
+
getCoreUpdate: (args) => request("GET", "/core-update", { hasBody: false }, args),
|
|
329
|
+
},
|
|
326
330
|
"cost": {
|
|
327
331
|
// POST /cost — rank: any-builder — POST /cost
|
|
328
332
|
postCost: (args) => request("POST", "/cost", { hasBody: true }, args),
|
|
@@ -50,6 +50,7 @@ export interface GetClosenessWithBuilderIdResponse { closeness: unknown; why: un
|
|
|
50
50
|
export interface GetConnectionsDegreeHandleResponse { handle: unknown; degree: unknown; mutuals: unknown; mutual_count: unknown; truncated: unknown }
|
|
51
51
|
export interface GetConnectionsRequestsResponse { incoming: unknown; outgoing: unknown; incoming_count: unknown; outgoing_count: unknown }
|
|
52
52
|
export interface GetConnectionsResponse { connections: unknown; count: unknown }
|
|
53
|
+
export interface GetCoreUpdateResponse { ok: boolean; update: unknown }
|
|
53
54
|
export interface GetDiscordChannelsPlanResponse { ok: boolean; guild_id: unknown; op_count: unknown; ops: unknown; warnings: unknown }
|
|
54
55
|
export interface GetDiscordChannelsSnapshotResponse { ok: boolean; guild_id: unknown; config: unknown; warnings: unknown }
|
|
55
56
|
export interface GetDoneWhenPendingReviewResponse { criteria: unknown; count: unknown }
|
|
@@ -689,6 +690,10 @@ export interface BongosClient {
|
|
|
689
690
|
/** GET /copy-desk/strings — rank: any-builder */
|
|
690
691
|
getCopyDeskStrings(args?: RequestArgs): Promise<ApiResponse>;
|
|
691
692
|
};
|
|
693
|
+
"coreUpdate": {
|
|
694
|
+
/** GET /core-update — rank: archon */
|
|
695
|
+
getCoreUpdate(args?: RequestArgs): Promise<GetCoreUpdateResponse>;
|
|
696
|
+
};
|
|
692
697
|
"cost": {
|
|
693
698
|
/** POST /cost — rank: any-builder */
|
|
694
699
|
postCost(args: RequestArgs & { body: PostCostRequest }): Promise<PostCostResponse>;
|
|
@@ -320,6 +320,10 @@ export function createClient(opts = {}) {
|
|
|
320
320
|
// GET /copy-desk/strings — rank: any-builder — GET /copy-desk/strings
|
|
321
321
|
getCopyDeskStrings: (args) => request("GET", "/copy-desk/strings", { hasBody: false }, args),
|
|
322
322
|
},
|
|
323
|
+
"coreUpdate": {
|
|
324
|
+
// GET /core-update — rank: archon — GET /core-update
|
|
325
|
+
getCoreUpdate: (args) => request("GET", "/core-update", { hasBody: false }, args),
|
|
326
|
+
},
|
|
323
327
|
"cost": {
|
|
324
328
|
// POST /cost — rank: any-builder — POST /cost
|
|
325
329
|
postCost: (args) => request("POST", "/cost", { hasBody: true }, args),
|
|
@@ -77,6 +77,16 @@ confirms the artifact. Anything not actually shipped releases the claim and file
|
|
|
77
77
|
a blocker, which also removes the task from the claimable pool so the loop does
|
|
78
78
|
not pick it straight back up.
|
|
79
79
|
|
|
80
|
+
Two things the implementation settled (`scripts/gds/autobongos-verify.js`, task
|
|
81
|
+
1003903). The mid-ship shapes are **not new names**: `strand-watch.js` already
|
|
82
|
+
called them `unprovable` / `unlanded` / `ungraded` and `/strand-fix` routes on
|
|
83
|
+
those strings, so `classifyStrand` is called directly and only its thresholds are
|
|
84
|
+
zeroed — the board sweep may wait an hour, the supervisor may not, because the
|
|
85
|
+
session that was supposed to finish has already exited. And the release must come
|
|
86
|
+
**before** the link: `linkBlockerToTask` parks a task only from `ready`, and
|
|
87
|
+
releasing is what puts it there, so linking first writes an edge that parks
|
|
88
|
+
nothing and the release then hands the task straight back to the loop.
|
|
89
|
+
|
|
80
90
|
### 5. There is no terminal stop — only speeds
|
|
81
91
|
|
|
82
92
|
On an always-on machine a stopped daemon is silently worthless. Three straight
|
package/docs/api/openapi.json
CHANGED
|
@@ -97,6 +97,10 @@
|
|
|
97
97
|
"name": "copy-desk",
|
|
98
98
|
"description": "Endpoints under `/api/bongos/copy-desk`."
|
|
99
99
|
},
|
|
100
|
+
{
|
|
101
|
+
"name": "core-update",
|
|
102
|
+
"description": "Endpoints under `/api/bongos/core-update`."
|
|
103
|
+
},
|
|
100
104
|
{
|
|
101
105
|
"name": "cost",
|
|
102
106
|
"description": "Endpoints under `/api/bongos/cost`."
|
|
@@ -4665,6 +4669,47 @@
|
|
|
4665
4669
|
]
|
|
4666
4670
|
}
|
|
4667
4671
|
},
|
|
4672
|
+
"/core-update": {
|
|
4673
|
+
"get": {
|
|
4674
|
+
"operationId": "get_core_update",
|
|
4675
|
+
"tags": [
|
|
4676
|
+
"core-update"
|
|
4677
|
+
],
|
|
4678
|
+
"summary": "GET /core-update",
|
|
4679
|
+
"description": "**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `core.pin.move` (all required).",
|
|
4680
|
+
"x-rank": "archon",
|
|
4681
|
+
"x-source": "src/bongos/routes/core-update.js",
|
|
4682
|
+
"x-permissions": [
|
|
4683
|
+
"core.pin.move"
|
|
4684
|
+
],
|
|
4685
|
+
"responses": {
|
|
4686
|
+
"200": {
|
|
4687
|
+
"description": "Success.",
|
|
4688
|
+
"content": {
|
|
4689
|
+
"application/json": {
|
|
4690
|
+
"schema": {
|
|
4691
|
+
"$ref": "#/components/schemas/GetCoreUpdateResponse"
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
},
|
|
4696
|
+
"400": {
|
|
4697
|
+
"$ref": "#/components/responses/BadRequest"
|
|
4698
|
+
},
|
|
4699
|
+
"401": {
|
|
4700
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
4701
|
+
},
|
|
4702
|
+
"403": {
|
|
4703
|
+
"$ref": "#/components/responses/Forbidden"
|
|
4704
|
+
}
|
|
4705
|
+
},
|
|
4706
|
+
"security": [
|
|
4707
|
+
{
|
|
4708
|
+
"builderSession": []
|
|
4709
|
+
}
|
|
4710
|
+
]
|
|
4711
|
+
}
|
|
4712
|
+
},
|
|
4668
4713
|
"/cost": {
|
|
4669
4714
|
"post": {
|
|
4670
4715
|
"operationId": "post_cost",
|
|
@@ -18718,6 +18763,19 @@
|
|
|
18718
18763
|
"count"
|
|
18719
18764
|
]
|
|
18720
18765
|
},
|
|
18766
|
+
"GetCoreUpdateResponse": {
|
|
18767
|
+
"type": "object",
|
|
18768
|
+
"properties": {
|
|
18769
|
+
"ok": {
|
|
18770
|
+
"type": "boolean"
|
|
18771
|
+
},
|
|
18772
|
+
"update": {}
|
|
18773
|
+
},
|
|
18774
|
+
"required": [
|
|
18775
|
+
"ok",
|
|
18776
|
+
"update"
|
|
18777
|
+
]
|
|
18778
|
+
},
|
|
18721
18779
|
"GetDiscordChannelsPlanResponse": {
|
|
18722
18780
|
"type": "object",
|
|
18723
18781
|
"properties": {
|
|
@@ -24938,9 +24996,9 @@
|
|
|
24938
24996
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
24939
24997
|
}
|
|
24940
24998
|
},
|
|
24941
|
-
"x-endpoint-count":
|
|
24942
|
-
"x-schema-count":
|
|
24999
|
+
"x-endpoint-count": 394,
|
|
25000
|
+
"x-schema-count": 433,
|
|
24943
25001
|
"x-undocumented-bodies": 9,
|
|
24944
|
-
"x-response-schemas":
|
|
25002
|
+
"x-response-schemas": 297,
|
|
24945
25003
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
|
24946
25004
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Bongos API reference
|
|
4
4
|
|
|
5
|
-
> **Generated from the live route files** — the route file is authoritative.
|
|
5
|
+
> **Generated from the live route files** — the route file is authoritative. 394 endpoints across 64 route files.
|
|
6
6
|
> Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
|
|
7
7
|
|
|
8
8
|
Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust-boundary-server-enforced-permissions.md)): `public` < `any-builder` < `metic+archon` < `archon`.
|
|
@@ -203,6 +203,12 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
203
203
|
| GET | `/api/bongos/copy-desk/queue` | `any-builder` | — | ------------------------------------------------------------------------- GET /copy-desk/queue — the artist's working list. |
|
|
204
204
|
| GET | `/api/bongos/copy-desk/strings` | `any-builder` | — | ------------------------------------------------------------------------- GET /copy-desk/strings — search the inventory, to find somethin… |
|
|
205
205
|
|
|
206
|
+
## `core-update` (1)
|
|
207
|
+
|
|
208
|
+
| Method | Path | Rank | Body | Description |
|
|
209
|
+
|---|---|---|---|---|
|
|
210
|
+
| GET | `/api/bongos/core-update` | `archon` | — | |
|
|
211
|
+
|
|
206
212
|
## `cost` (1)
|
|
207
213
|
|
|
208
214
|
| Method | Path | Rank | Body | Description |
|
package/docs/architecture.md
CHANGED
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
**Live & running:**
|
|
11
11
|
|
|
12
|
-
- **Domain:** `example.com` — registered at Cloudflare Registrar. DNS A records (apex + `www`) point to the droplet at `REDACTED_IP`. Cloudflare proxy is **ON (orange cloud)**. CF terminates TLS at the edge; the **Cloudflare Origin Certificate** handles the CF↔origin leg (valid through 2041-05-04). A `status` subdomain is added at PMS-V1 deploy time (see
|
|
12
|
+
- **Domain:** `example.com` — registered at Cloudflare Registrar. DNS A records (apex + `www`) point to the droplet at `REDACTED_IP`. Cloudflare proxy is **ON (orange cloud)**. CF terminates TLS at the edge; the **Cloudflare Origin Certificate** handles the CF↔origin leg (valid through 2041-05-04). A `status` subdomain is added at PMS-V1 deploy time (see `docs/pms-v1-deploy.md` (instance-side)) with the same proxy/cert posture; Origin Cert needs `*.example.com` SAN. Real client IPs come in via `CF-Connecting-IP` / `X-Forwarded-For`, wired into Caddy's `trusted_proxies static` (the Cloudflare IP ranges) in the `infra/Caddyfile` global options block.
|
|
13
13
|
- **Production droplet (co-hosting box):** DigitalOcean, hostname `example`, IPv4 `REDACTED_IP`, IPv6 `REDACTED_IP`. NYC3, Premium Intel 2 GB / 1 vCPU / 60 GB NVMe. Ubuntu 24.04 LTS. ~$14/mo. Hosts the OTB game + co-hosted project instances (`emersonian-circles.`, `staging.`).
|
|
14
14
|
- **Cloud Bongos control-plane droplet (NEW 2026-07-05, [ADR 0126](adr/0126-dedicated-cloudbongos-control-plane-droplet.md), task [#2063](https://example.com/builders#/task/2063)):** DigitalOcean, hostname `cloudbongos`, IPv4 `REDACTED_IP`, NYC3, `s-1vcpu-2gb` (2 GB / 1 vCPU), Ubuntu 24.04, ~$12/mo, tag `cloudbongos-control`. Runs **cloudbongos.com + `builders.` + `status.`** (`src/platform-server.js`, Cloud Bongos brand, port 3002, own `cloudbongos` DB, checkout `/home/lars/cloudbongos-instance`, the root subscribed to the nightly core-patch sweep) and is the **sole token-holding control plane** — a SECOND root, `/home/lars/cloudbongos`, survives only to vendor `infra/` (the box cloud-init the core package does not ship, ADR 0150); it is not subscribed, so nothing may resolve core code from it (task 1002738, [ADR 0144](adr/0144-devbox-rehome-onto-cloudbongos-plane.md) follow-up 5) — `/etc/cloudbongos/box.env` holds the account-wide Cloudflare token (IP-locked to this box) + the DigitalOcean token; this is where `provision.js` provisions instance subdomains. **Structurally separate SSH** (own `cloudbongos_ed25519` key). IPv6 egress disabled so the IPv4-locked CF token can't be spuriously rejected. cloudbongos.com was previously a co-tenant on the co-hosting box; that unit was decommissioned at cutover. **Follow-ups:** deploy automation (task [#2064](https://example.com/builders#/task/2064)) + fully stripping cloud tokens off the co-hosting box (task [#2065](https://example.com/builders#/task/2065)).
|
|
15
15
|
- **Server stack:** Node.js 22 LTS · PostgreSQL 16 · Caddy 2.11.2. `example.service` (systemd) runs `node server.js` as user `lars` on `127.0.0.1:3000`; restart-on-failure.
|
|
16
16
|
- **Hardening:** `lars` user with passwordless sudo; SSH key-only (root login disabled, password auth disabled); ufw allows 22/80/443; fail2ban active; unattended-upgrades enabled; timezone `America/New_York`.
|
|
17
17
|
- **TLS:** Origin cert at `/etc/caddy/origin.crt` + `/etc/caddy/origin.key` for the example apexes on the co-hosting box. **cloudbongos.com's apexes moved to the dedicated control-plane droplet** ([ADR 0126](adr/0126-dedicated-cloudbongos-control-plane-droplet.md)) with their own copy of the Cloudflare Origin Cert (SAN `*.cloudbongos.com, cloudbongos.com`) at `/etc/caddy/cloudbongos-origin.{crt,key}` on `REDACTED_IP`. Cloudflare SSL/TLS mode = Full (strict). Renewal is a non-issue for ~15 years. **Provisioned instance hosts use a different, coexisting posture** ([ADR 0111](adr/0111-instance-hosting-provisioning-module.md) §4 / task P4): Caddy **on-demand ACME** (Let's Encrypt) via the global `on_demand_tls { ask … }` block, gated to hostnames the `provisioning` module actually stood up (`/api/bongos/provisioning/tls-check`). Their DNS records are **proxied=false (grey cloud)** so the ACME challenge reaches Caddy — the opposite of the origin-cert apexes; do not enable the proxy on them or renewal breaks silently ([ADR 0003](adr/0003-cloudflare-origin-cert-over-letsencrypt.md)). Per-instance site snippets live in `/etc/caddy/sites/*.caddy` (`import`ed by `infra/Caddyfile`); the provisioning runner verifies the box's resolved Caddy config carries that import and the `on_demand_tls` ask before it reports a domain wired, and refuses with the line to add rather than editing the base file ([ADR 0303](adr/0303-provisioning-verifies-the-base-caddyfile-and-never-edits-it.md)). **Tradeoff (accepted, SEC review of task 1945):** `proxied=false` publishes the box's real origin IP in public DNS for every co-tenant provisioned hostname — defeating, for those names, the Cloudflare-proxy IP-hiding the apexes enjoy (a co-tenant host resolves to the same `REDACTED_IP` that serves the main game). This is inherent to on-demand ACME (the challenge must hit the origin) and acceptable under the budget/trust model; prefer a **dedicated droplet** (its own IP) for any instance expected to draw hostile attention.
|
|
18
|
-
- **Platform observability — REMOVED 2026-09-15 ([ADR 0284](adr/0284-the-control-plane-carries-no-metrics-stack.md), task 1003876):** the control plane runs **no Grafana and no Prometheus**, and `metrics.cloudbongos.com` has no Caddy site. They cost ~300 MB RSS + 2.5 GB of data on the box every provisioned project shares, and scraped none of those projects. Every instance still exports `/metrics` ([`src/bongos/metrics.js`](../src/bongos/metrics.js), loopback or `METRICS_TOKEN`), so a scraper can return from a box that is *not* the control plane. The rest of this bullet is the **historical** ADR 0127 layout, kept so its leftovers (co-hosting-box forwarders, instance `infra/` copies) can be found: this watched the **Bongos API** (`gds_http`/`tasks`/`claims`/`builders`/`auth`), not any one game, so it lived **on the control-plane box** (REDACTED_IP) at **`https://metrics.cloudbongos.com`** and observed the **whole fleet**. **Prometheus** (`127.0.0.1:9090`, loopback-only) scrapes every instance's `/metrics` ([`src/bongos/metrics.js`](../src/bongos/metrics.js)), each tagged with a `project` label (
|
|
18
|
+
- **Platform observability — REMOVED 2026-09-15 ([ADR 0284](adr/0284-the-control-plane-carries-no-metrics-stack.md), task 1003876):** the control plane runs **no Grafana and no Prometheus**, and `metrics.cloudbongos.com` has no Caddy site. They cost ~300 MB RSS + 2.5 GB of data on the box every provisioned project shares, and scraped none of those projects. Every instance still exports `/metrics` ([`src/bongos/metrics.js`](../src/bongos/metrics.js), loopback or `METRICS_TOKEN`), so a scraper can return from a box that is *not* the control plane. The rest of this bullet is the **historical** ADR 0127 layout, kept so its leftovers (co-hosting-box forwarders, instance `infra/` copies) can be found: this watched the **Bongos API** (`gds_http`/`tasks`/`claims`/`builders`/`auth`), not any one game, so it lived **on the control-plane box** (REDACTED_IP) at **`https://metrics.cloudbongos.com`** and observed the **whole fleet**. **Prometheus** (`127.0.0.1:9090`, loopback-only) scrapes every instance's `/metrics` ([`src/bongos/metrics.js`](../src/bongos/metrics.js)), each tagged with a `project` label (`infra/prometheus/prometheus.yml` (instance-side)): **cloudbongos** locally (`:3002`); **example + emersonian-circles** on the co-hosting box, reached over the **private VPC** via `socat` forwarders on `10.108.0.2:9101/9102` (→ their loopback `:3000`/`:3004`), ufw-locked to the control-plane box's private IP — raw TCP relays, so the exporter still sees a loopback request and needs no token; nothing is public. **Grafana** (`127.0.0.1:3003`, loopback-only) renders the 4 provisioned-as-code dashboards (folder **Cloud Bongos**: system-overview, error-watch, builder-activity, auth-security), each with a **`project`** template variable for per-instance / fleet views. Reverse-proxied on the `*.cloudbongos.com` origin cert — Archon-only, gated by Caddy `basic_auth` (user `archon`) **and** Grafana's own admin login (both secrets owner-held, never in the repo). Grafana config is a systemd `GF_*` env drop-in (`grafana-server.service.d/override.conf`, `chmod 600`). ~256 MB RAM. A future dedicated-droplet instance is added as a `METRICS_TOKEN` Bearer target instead of a VPC forwarder.
|
|
19
19
|
|
|
20
20
|
**Application stack:**
|
|
21
21
|
|
|
@@ -65,7 +65,7 @@ The product is organized as a small kernel (`src/`) + **19 modules** under `modu
|
|
|
65
65
|
**Code & deploy:**
|
|
66
66
|
|
|
67
67
|
- **Code repo:** [github.com/example-owner/example](https://github.com/example-owner/example) — **private**. Clone URL: `git@github.com:example-owner/example.git`. Default branch: `main`. GitHub user: `example-owner`.
|
|
68
|
-
- **Deploy:** two modes, selected by `config/deploy.json` `mode` (env `OTB_DEPLOY_MODE` overrides), currently **`ci`** (flipped from `laptop` 2026-06-13, [#859](https://example.com/builders#/task/859)) — see [ADR 0042](adr/0042-builder-self-deploy-ci-auto-merge.md). **`laptop`** (legacy): `git push origin main` from the Mac → `ssh lars@REDACTED_IP ~/deploy.sh`. The droplet has a read-only deploy key at `~/.ssh/github_deploy` registered on the repo. `~/deploy.sh` does: `git fetch + reset --hard origin/main` → `npm ci --omit=dev` if `package-lock.json` changed → `./scripts/migrate.sh` → `sudo systemctl restart example` → `curl /healthz` smoke (**retried up to 15× @ 1s** — `systemctl` reports "active" before the Node process binds the port, so a single curl raced the bind and false-failed healthy deploys: [ADR 0056](adr/0056-prod-deploy-script-mirror-and-healthcheck-retry.md) / [#1026](https://example.com/builders#/task/1026)). Droplet checkout: `/home/lars/example/`. `~/deploy.sh` is hand-maintained on the droplet but now has a byte-faithful, reviewable repo mirror at
|
|
68
|
+
- **Deploy:** two modes, selected by `config/deploy.json` `mode` (env `OTB_DEPLOY_MODE` overrides), currently **`ci`** (flipped from `laptop` 2026-06-13, [#859](https://example.com/builders#/task/859)) — see [ADR 0042](adr/0042-builder-self-deploy-ci-auto-merge.md). **`laptop`** (legacy): `git push origin main` from the Mac → `ssh lars@REDACTED_IP ~/deploy.sh`. The droplet has a read-only deploy key at `~/.ssh/github_deploy` registered on the repo. `~/deploy.sh` does: `git fetch + reset --hard origin/main` → `npm ci --omit=dev` if `package-lock.json` changed → `./scripts/migrate.sh` → `sudo systemctl restart example` → `curl /healthz` smoke (**retried up to 15× @ 1s** — `systemctl` reports "active" before the Node process binds the port, so a single curl raced the bind and false-failed healthy deploys: [ADR 0056](adr/0056-prod-deploy-script-mirror-and-healthcheck-retry.md) / [#1026](https://example.com/builders#/task/1026)). Droplet checkout: `/home/lars/example/`. `~/deploy.sh` is hand-maintained on the droplet but now has a byte-faithful, reviewable repo mirror at `scripts/deploy/deploy-prod.sh` (instance-side) — **not** auto-installed (the ci deploy key is forced-command-locked, ADR 0042/0043; reinstall by hand on change: `scp scripts/deploy/deploy-prod.sh lars@…:deploy.sh`). **`ci`** (current, armed via [#679](https://example.com/builders#/task/679)): no builder box holds the droplet key — `ship.js` opens a PR + enables GitHub auto-merge, and `.github/workflows/deploy-prod.yml` (instance-side) runs the same `~/deploy.sh` from Actions on merge to `main`. Cutover preconditions (incl. a server-recorded **CI grade-gate**) are in ADR 0042.
|
|
69
69
|
- **Post-merge branch cleanup (standard practice):** Every feature branch that lands on main via `--no-ff` merge gets deleted on origin once the deploy succeeds. The merge commit itself preserves the branch's history in main, so the named ref isn't carrying any signal — keeping it just clutters GitHub's branch list and tricks the UI into offering empty PRs against zero-diff branches. The auto-merge in [`scripts/gds/ship.js`](../scripts/gds/ship.js) does this automatically after `~/deploy.sh` returns 0; the manual [`/merge-mode`](../.claude/skills/merge-mode/SKILL.md) flow follows the same step 5b. Local refs and worktree directories stay — only the origin branch and the local tracking ref (cleaned on next `git fetch --prune`) go.
|
|
70
70
|
- **Local dev machine:** Lars's MacBook Air (Apple Silicon, macOS Sequoia). Tooling installed via Homebrew at `/opt/homebrew/`. Node v26.0.0 locally; `package.json` declares `"engines": { "node": ">=22" }`. Single ed25519 SSH key (`~/.ssh/id_ed25519`) authenticates to GitHub and droplet.
|
|
71
71
|
- **Smoke tests:** lightweight scripts in `/tmp/colyseus-*-smoketest.js` verify joinOrCreate, move protocol, interact, identity, queue. Run any after deploy with `node /tmp/colyseus-<name>-smoketest.js`.
|
|
@@ -134,7 +134,7 @@ session_logs(id, builder_id, task_id?, claim_id?, started_at, ended_at, outcome,
|
|
|
134
134
|
notes_md, confidence)
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
PMS-V2 additions (migration `006_pms_v2.sql`, see
|
|
137
|
+
PMS-V2 additions (migration `006_pms_v2.sql`, see `limitations/pms-v2-shipped.md` (instance-side)):
|
|
138
138
|
|
|
139
139
|
```
|
|
140
140
|
-- new columns
|
|
@@ -240,7 +240,7 @@ failure modes) and [ADR 0016](adr/0016-trust-boundary-server-enforced-permission
|
|
|
240
240
|
**Authority lives in `builders.rank` on the prod DB, checked server-side per request with no caching;
|
|
241
241
|
markdown describes, the DB enforces.**
|
|
242
242
|
|
|
243
|
-
> **Two entrypoints, one serving path (ADR 0062 R51, task 1190).** The host-routing + hall/status serving below is factored into `mountInternalSurfaces(app)` in [`src/bongos/serve-internal.js`](../src/bongos/serve-internal.js), called by **both** the game boot (
|
|
243
|
+
> **Two entrypoints, one serving path (ADR 0062 R51, task 1190).** The host-routing + hall/status serving below is factored into `mountInternalSurfaces(app)` in [`src/bongos/serve-internal.js`](../src/bongos/serve-internal.js), called by **both** the game boot (`server.js` (instance-side)) and the new **Bongos-only boot** ([`src/platform-server.js`](../src/platform-server.js)). The platform boot constructs **no Colyseus, no game rooms, no `/vendor/*`, no `/assets/tiles`, no `public/`** — a game-less Cloud Bongos instance runs it unchanged, and "no game route is reachable" holds by absence (the inverse of `preview-server.js`, which serves only the game). Prod still runs `node server.js`; Bongos-only entrypoint is the seam the instance model (R54/R67) boots on.
|
|
244
244
|
|
|
245
245
|
**Multi-host setup (same Node process), routed by `Host` header in [`src/bongos/serve-internal.js`](../src/bongos/serve-internal.js), shared by both entrypoints:**
|
|
246
246
|
|
|
@@ -248,9 +248,9 @@ markdown describes, the DB enforces.**
|
|
|
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
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.
|
|
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
|
-
- **`/downloads/devbox/*`** (every host, mounted right after the API so the builders-host shim can't rewrite it — [#904](https://example.com/builders#/task/904), [ADR 0045](adr/0045-devbox-desktop-app.md)): branded download surface for the Dev Box desktop app. `/{mac-arm64,mac-x64,win-x64}` (+ `mac`/`win` aliases) 302 to the release assets on the public releases repo named in
|
|
251
|
+
- **`/downloads/devbox/*`** (every host, mounted right after the API so the builders-host shim can't rewrite it — [#904](https://example.com/builders#/task/904), [ADR 0045](adr/0045-devbox-desktop-app.md)): branded download surface for the Dev Box desktop app. `/{mac-arm64,mac-x64,win-x64}` (+ `mac`/`win` aliases) 302 to the release assets on the public releases repo named in `config/devbox-app.json` (instance-side); `latest.json` feeds the app's update check. Logic in [`src/devbox-downloads.js`](../src/devbox-downloads.js); the installers never ride the git deploy.
|
|
252
252
|
- 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.
|
|
253
|
-
- Caddy gets a site block per host (
|
|
253
|
+
- Caddy gets a site block per host (`infra/Caddyfile` (instance-side)), all reverse-proxying to `127.0.0.1:3000`.
|
|
254
254
|
|
|
255
255
|
**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.
|
|
256
256
|
|
|
@@ -28,7 +28,7 @@ env override → instance config (config/branding.json) → neutral star
|
|
|
28
28
|
| `domains.statusDataUrl` | **Optional.** The off-box prober's `status.json` URL that the mirrored status page's availability banner polls (ADR 0029). Read by `scripts/status-mirror-sync.js`; `""` (the neutral default) = this instance publishes no off-box mirror, and the publisher REFUSES rather than shipping a banner that points nowhere. (task [#1002510](https://cloudbongos.com/builders#/task/1002510)) |
|
|
29
29
|
| `domains.cookieDomain` | Session cookie domain (`""` = host-only — the safe default; never widen to a parent domain by accident). |
|
|
30
30
|
| `domains.oauthOrigin` | Canonical sign-in origin. |
|
|
31
|
-
| `domains.provisioningOrigin` | **Optional.** Control-plane front-door origin the [provisioning module](
|
|
31
|
+
| `domains.provisioningOrigin` | **Optional.** Control-plane front-door origin the [provisioning module](../modules/provisioning/CLAUDE.md) uses to build a new customer's GitHub-App `redirect_url`. Set it when the control plane co-hosts a game whose `publicOrigin` is the game's own domain (OTB = customer 0) so the customer's App carries the PLATFORM brand (e.g. `https://get.cloudbongos.com`), not the game's. **Falls back to `publicOrigin` when unset** — an instance whose `publicOrigin` is already the platform needs no override. Env-overridable: `<PREFIX>_PROVISIONING_ORIGIN`. ([#2101](https://example.com/builders#/task/2101)) |
|
|
32
32
|
| `domains.devBoxBase` | Apex zone the **proxied** browser-terminal + sandbox hosts ride (`term-<login>.<devBoxBase>`, `sandbox-<login>.<devBoxBase>`). Must be a one-label-under-apex zone so free edge TLS (`*.<apex>`) covers it — a two-label host fails edge TLS (the [#771](https://example.com/builders#/task/771) lesson). Empty ⇒ no managed boxes. |
|
|
33
33
|
| `domains.sshBoxBase` | Zone for the **legacy direct-IP SSH** box host (`box-<login>.<sshBoxBase>`, unproxied). Deliberately distinct from `devBoxBase` (one proxied/edge-TLS, one direct-IP). |
|
|
34
34
|
| `domains.instanceBase` | Apex zone new projects get their **auto-assigned** address under (`<slug>.<instanceBase>`, task 1002704 — every project's hall reachable day one; `no_address: true` on the create call opts out). Same one-label-under-apex/edge-TLS constraint as `devBoxBase`. Deliberately NOT derived from `publicOrigin` (on a co-hosting control plane that is the game's domain). Empty ⇒ no auto-assign — creates are domainless unless the caller brings a domain. |
|