@bongos/core 1.19.773 → 1.19.774

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 (90) hide show
  1. package/.bongos-core.json +189 -74
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +5 -1
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/adr/0083-modular-architecture-strangler-build-plan.md +1 -0
  8. package/docs/adr/0177-goal-membership-kind.md +26 -2
  9. package/docs/adr/0181-teardown-state-gate-and-address-retention.md +2 -0
  10. package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -1
  11. package/docs/adr/0288-the-goal-wall-has-a-shared-floor-for-paths-no-module-owns.md +33 -0
  12. package/docs/adr/0289-an-unconsumed-contribution-is-a-ci-failure-not-a-boot-refusal.md +33 -0
  13. package/docs/adr/0290-disconnect-is-the-owner-initiated-exception-to-address-retention.md +40 -0
  14. package/docs/adr/README.md +3 -0
  15. package/docs/api/openapi.json +102 -11
  16. package/docs/api-reference.md +5 -4
  17. package/docs/copy-inventory.md +205 -190
  18. package/docs/copy-registry.json +435 -269
  19. package/docs/file-map.md +1 -0
  20. package/docs/module-api-changelog.md +2 -0
  21. package/docs/modules-contract.md +1 -1
  22. package/modules/builder-settings/render-prefs.js +281 -33
  23. package/modules/builder-settings/routes/settings.js +20 -12
  24. package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
  25. package/modules/dev-box/module.json +4 -0
  26. package/modules/hall-ui/public/settings-render.js +138 -0
  27. package/modules/hall-ui/public/settings.html +16 -9
  28. package/modules/hall-ui/public/settings.js +3 -70
  29. package/modules/lifecycle/ship-card.js +49 -13
  30. package/modules/platform-identity/migrations/platform_identity_021_drop_legacy_role.sql +94 -0
  31. package/modules/platform-identity/platform-identity.js +8 -19
  32. package/modules/platform-identity/project-catalog.js +24 -0
  33. package/modules/platform-identity/routes/sso.js +11 -14
  34. package/modules/platform-identity/tests/platform-identity.mjs +7 -9
  35. package/modules/provisioning/capacity-gate.js +54 -0
  36. package/modules/provisioning/catalog-bridge.js +2 -0
  37. package/modules/provisioning/disconnect.js +35 -0
  38. package/modules/provisioning/migrations/provisioning_016_disconnect.sql +21 -0
  39. package/modules/provisioning/module.json +1 -1
  40. package/modules/provisioning/provisioning.js +3 -3
  41. package/modules/provisioning/routes/disconnect.js +63 -0
  42. package/modules/provisioning/routes/provisioning.js +6 -6
  43. package/modules/public-landing/public/projects.html +116 -11
  44. package/modules/public-landing/public/projects.probes.json +3 -0
  45. package/modules/ui-design/kit/serve.js +3 -2
  46. package/modules/ui-design/module.json +1 -0
  47. package/package-lock.json +2 -2
  48. package/package.json +1 -1
  49. package/scripts/gds/address-park-sweep.sh +78 -0
  50. package/scripts/gds/cli-lib.js +37 -0
  51. package/scripts/gds/fitness-migration-contraction.js +50 -0
  52. package/scripts/gds/fitness.js +18 -0
  53. package/scripts/gds/land-watch.js +8 -2
  54. package/scripts/gds/provision-config.js +4 -4
  55. package/scripts/gds/provision-disconnect.js +90 -0
  56. package/scripts/gds/provision-teardown.js +108 -0
  57. package/scripts/gds/provision.js +6 -65
  58. package/scripts/gds/seam-wiring-guard.js +273 -0
  59. package/scripts/gds/ship-finish.js +17 -10
  60. package/scripts/gds/ship-flow.js +5 -5
  61. package/scripts/gds/start.js +13 -7
  62. package/src/bongos/auth-admission.js +7 -8
  63. package/src/bongos/auth-config.js +3 -2
  64. package/src/bongos/goal-scope-check.js +23 -8
  65. package/src/bongos/module-scope-map.js +40 -0
  66. package/src/module-api.js +1 -1
  67. package/src/module-loader/manifest-schema.js +79 -1
  68. package/tests/catalog_only_client.mjs +8 -5
  69. package/tests/fitness.mjs +22 -0
  70. package/tests/goal_scope_check.mjs +63 -1
  71. package/tests/hall_settings_world.mjs +57 -1
  72. package/tests/hub_disconnect_ui.mjs +105 -0
  73. package/tests/land_watch.mjs +44 -0
  74. package/tests/membership_kind_lockstep.mjs +86 -27
  75. package/tests/module-scope-map.mjs +39 -0
  76. package/tests/module_exports_unique_keys.mjs +70 -0
  77. package/tests/module_manifest.mjs +85 -0
  78. package/tests/project_catalog_forget.mjs +83 -0
  79. package/tests/projects_hub.mjs +10 -4
  80. package/tests/projects_hub_pre_uat.mjs +5 -3
  81. package/tests/provision_disconnect.mjs +231 -0
  82. package/tests/provision_teardown_address.mjs +230 -0
  83. package/tests/provisioning_capacity_gate.mjs +192 -0
  84. package/tests/provisioning_disconnect.mjs +231 -0
  85. package/tests/provisioning_restart.mjs +5 -5
  86. package/tests/public_landing_projects.mjs +4 -3
  87. package/tests/render_prefs.mjs +152 -0
  88. package/tests/seam_wiring_guard.mjs +234 -0
  89. package/tests/ship_card.mjs +67 -1
  90. package/tests/sso_checkin_legacy_field_gone.mjs +246 -0
@@ -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
- - 373 operations across 58 resource groups
8
+ - 374 operations across 58 resource groups
9
9
 
10
10
  ## Use it from your project
11
11
 
@@ -705,6 +705,8 @@ function createClient(opts = {}) {
705
705
  getProvisioningInstancesId: (args) => request("GET", "/provisioning/instances/{id}", { hasBody: false }, args),
706
706
  // PATCH /provisioning/instances/{id}/detail — rank: any-builder — PATCH /provisioning/instances/:id/detail
707
707
  patchProvisioningInstancesIdDetail: (args) => request("PATCH", "/provisioning/instances/{id}/detail", { hasBody: true }, args),
708
+ // POST /provisioning/instances/{id}/disconnect — rank: any-builder — POST /provisioning/instances/:id/disconnect
709
+ postProvisioningInstancesIdDisconnect: (args) => request("POST", "/provisioning/instances/{id}/disconnect", { hasBody: true }, args),
708
710
  // POST /provisioning/instances/{id}/domain — rank: any-builder — POST /provisioning/instances/:id/domain
709
711
  postProvisioningInstancesIdDomain: (args) => request("POST", "/provisioning/instances/{id}/domain", { hasBody: true }, args),
710
712
  // POST /provisioning/instances/{id}/force-teardown — rank: metic+archon — POST /provisioning/instances/:id/force-teardown
@@ -704,6 +704,8 @@ function createClient(opts = {}) {
704
704
  getProvisioningInstancesId: (args) => request("GET", "/provisioning/instances/{id}", { hasBody: false }, args),
705
705
  // PATCH /provisioning/instances/{id}/detail — rank: any-builder — PATCH /provisioning/instances/:id/detail
706
706
  patchProvisioningInstancesIdDetail: (args) => request("PATCH", "/provisioning/instances/{id}/detail", { hasBody: true }, args),
707
+ // POST /provisioning/instances/{id}/disconnect — rank: any-builder — POST /provisioning/instances/:id/disconnect
708
+ postProvisioningInstancesIdDisconnect: (args) => request("POST", "/provisioning/instances/{id}/disconnect", { hasBody: true }, args),
707
709
  // POST /provisioning/instances/{id}/domain — rank: any-builder — POST /provisioning/instances/:id/domain
708
710
  postProvisioningInstancesIdDomain: (args) => request("POST", "/provisioning/instances/{id}/domain", { hasBody: true }, args),
709
711
  // POST /provisioning/instances/{id}/force-teardown — rank: metic+archon — POST /provisioning/instances/:id/force-teardown
@@ -343,6 +343,8 @@ export interface PostOverrideRequestsIdDecideRequest { decision: string; note?:
343
343
  export interface PostProjectsInviteRequest { client_id: string; github_login: string }
344
344
  export interface PostProjectsRequest { origin: string; name: string; tagline?: string; description?: string; art_url?: string; type?: string; template?: string }
345
345
  export interface PostProjectsResponse { project: unknown }
346
+ export interface PostProvisioningInstancesIdDisconnectRequest { confirm_slug: string }
347
+ export interface PostProvisioningInstancesIdDisconnectResponse { ok: boolean; queued: unknown; action: unknown; status: unknown; message: unknown }
346
348
  export interface PostProvisioningInstancesIdDomainRequest { domain: string }
347
349
  export interface PostProvisioningInstancesIdDomainResponse { ok: boolean; changed: boolean; queued: unknown; domain: unknown; message: unknown; replaced?: unknown }
348
350
  export interface PostProvisioningInstancesIdForceTeardownResponse { ok: boolean; queued: unknown; action: unknown; status: unknown; superseded: unknown; message: unknown }
@@ -375,7 +377,7 @@ export interface PostSsoApplicantProfileRequest { client_id: string; client_secr
375
377
  export interface PostSsoApplicantProfileResponse { profile: unknown }
376
378
  export interface PostSsoDevicePollRequest { device_code: string }
377
379
  export interface PostSsoDeviceStartRequest { origin: string }
378
- export interface PostSsoMembershipCheckInRequest { client_id: string; client_secret: string; github_id: StringifiableId; github_login: string; display_name?: string; avatar_url?: string; membership_kind?: string; role?: string }
380
+ export interface PostSsoMembershipCheckInRequest { client_id: string; client_secret: string; github_id: StringifiableId; github_login: string; display_name?: string; avatar_url?: string; membership_kind?: string }
379
381
  export interface PostSsoMembershipCheckInResponse { ok: boolean }
380
382
  export interface PostSsoTokenRequest { client_id: string; client_secret: string; code: string; redirect_uri: string }
381
383
  export interface PostSsoTokenResponse { github_id: unknown; github_login: unknown; display_name: unknown; avatar_url: unknown }
@@ -1050,6 +1052,8 @@ export interface BongosClient {
1050
1052
  getProvisioningInstancesId(args?: RequestArgs): Promise<GetProvisioningInstancesIdResponse>;
1051
1053
  /** PATCH /provisioning/instances/{id}/detail — rank: any-builder */
1052
1054
  patchProvisioningInstancesIdDetail(args?: RequestArgs & { body?: PatchProvisioningInstancesIdDetailRequest }): Promise<PatchProvisioningInstancesIdDetailResponse>;
1055
+ /** POST /provisioning/instances/{id}/disconnect — rank: any-builder */
1056
+ postProvisioningInstancesIdDisconnect(args: RequestArgs & { body: PostProvisioningInstancesIdDisconnectRequest }): Promise<PostProvisioningInstancesIdDisconnectResponse>;
1053
1057
  /** POST /provisioning/instances/{id}/domain — rank: any-builder */
1054
1058
  postProvisioningInstancesIdDomain(args: RequestArgs & { body: PostProvisioningInstancesIdDomainRequest }): Promise<PostProvisioningInstancesIdDomainResponse>;
1055
1059
  /** POST /provisioning/instances/{id}/force-teardown — rank: metic+archon */
@@ -701,6 +701,8 @@ export function createClient(opts = {}) {
701
701
  getProvisioningInstancesId: (args) => request("GET", "/provisioning/instances/{id}", { hasBody: false }, args),
702
702
  // PATCH /provisioning/instances/{id}/detail — rank: any-builder — PATCH /provisioning/instances/:id/detail
703
703
  patchProvisioningInstancesIdDetail: (args) => request("PATCH", "/provisioning/instances/{id}/detail", { hasBody: true }, args),
704
+ // POST /provisioning/instances/{id}/disconnect — rank: any-builder — POST /provisioning/instances/:id/disconnect
705
+ postProvisioningInstancesIdDisconnect: (args) => request("POST", "/provisioning/instances/{id}/disconnect", { hasBody: true }, args),
704
706
  // POST /provisioning/instances/{id}/domain — rank: any-builder — POST /provisioning/instances/:id/domain
705
707
  postProvisioningInstancesIdDomain: (args) => request("POST", "/provisioning/instances/{id}/domain", { hasBody: true }, args),
706
708
  // POST /provisioning/instances/{id}/force-teardown — rank: metic+archon — POST /provisioning/instances/:id/force-teardown
@@ -74,6 +74,7 @@ Generalize the working `migrations/game/` precedent ([02 §1 grounded](../design
74
74
 
75
75
  - Teach `scripts/migrate.sh` to discover `modules/*/migrations/*.sql` and apply each **only if its module is enabled** (the `MEDUSA_GDS_ONLY` gate generalized to per-module enablement). `schema_migrations` stays **stem-keyed**, so relocating a migration from `migrations/game/` to `modules/game/migrations/` does **not** re-run it on prod — the existing safety guarantee carries the move for free.
76
76
  - **Disable preserves data** — the entire mechanism is "don't apply," never "drop." There is no down-migration path and we keep it that way; a disabled module's tables simply are never created (or are left untouched).
77
+ - *Amended 2026-09-16 (task [1003092](https://cloudbongos.com/builders#/task/1003092)):* the additive rule met its first legitimate exception when a module ran an expand-then-contract rename ([ADR 0177](0177-goal-membership-kind.md) §"the fifth use" — `<redacted>` expanded, `<redacted>` contracted). A contract phase must drop the legacy column or the rename never finishes. The fitness check therefore admits **exactly one** destructive shape: `ALTER TABLE <own namespaced table> DROP COLUMN <c>`, one statement, in a file carrying the `-- forward-unsafe: <reason>` pragma core migrations already use for the same acknowledgement. Nothing else changes — `DROP TABLE`, `TRUNCATE` and a bare `DROP CONSTRAINT` are still refused under the pragma, and the pragma is per-statement permission for that shape, not a per-file switch (`scripts/gds/fitness-migration-contraction.js`, negative-tested in `tests/fitness.mjs`).
77
78
  - Generalize `scripts/verify-gds-only-schema.sh` into a per-module schema-presence/absence verifier.
78
79
  - `scripts/migrate.sh` is a **gate HARD-FLOOR** (`gate-review.js`) and `migrations/` is **NEEDS_TRUST + a protected path** (`permission-path-check.js:78-80`): the runner change needs owner sign-off; new `modules/<key>/migrations/` subtrees inherit the protected-path treatment. The loader/migrate change is owner-gated by construction.
79
80
 
@@ -136,11 +136,35 @@ row was ever affected because the pin still served 1.19.218; the invariant is
136
136
  pinned statically by `tests/membership_kind_lockstep.mjs`, since the trigger only
137
137
  misbehaves against a live Postgres and CI has none.
138
138
 
139
- The window's removal is a filed task, and its condition is explicit: every federated
139
+ The window's removal was a filed task, and its condition was explicit: every federated
140
140
  instance past the window, not merely "one release later". A time-boxed shim still ships
141
141
  with the test of its absence (ADR 0174's Lineage lesson) — that test lands with the
142
142
  removal, because until then the legacy field is load-bearing, not vestigial.
143
143
 
144
+ ### The window closed — 2026-09-16, task [1003092](https://cloudbongos.com/builders#/task/1003092)
145
+
146
+ `<redacted>` is the contract phase: the lockstep trigger and its function
147
+ are dropped, `<redacted>.role` is dropped, and
148
+ `membership_kind` gains `NOT NULL` + `DEFAULT 'member'` — the two guarantees 010
149
+ deferred. The check-in route's schema no longer names `role` (strict validation
150
+ refuses a legacy-only body with 400 and writes nothing), the sender writes one
151
+ spelling, and the translator functions are deleted.
152
+
153
+ **How the condition was satisfied, since it was a condition and not a date.** The
154
+ hub holds no record of a caller's core version, so the audit probed `/version` on
155
+ every origin in the hub's project catalogue. The release carrying the expand phase is
156
+ core 1.19.241 (2026-08-18). The hub served 1.19.766; every hosted instance served
157
+ 1.19.312 or later. Two catalogued instances — the original standalone Example
158
+ (core 1.17.3) and Emersonian Circles (no `coreVersion` at all) — sat *below* the
159
+ window, and that turned out not to matter: federation itself (the hub SSO, the client
160
+ registry, the idp client config and the check-in sender) landed on 2026-07-13, after
161
+ the 1.18.0 re-seed, so a 1.17.x core has no idp config, registers as no sso_client and
162
+ has never sent a check-in of either spelling. They are catalogue-only rows. One hosted
163
+ box (walk-lantern) was unreachable; if it wakes below 1.19.241 it must be upgraded
164
+ before anyone signs in to it. The general lesson for the next cross-version window:
165
+ "behind the release" and "a live legacy sender" are different sets, and the second is
166
+ the gating one.
167
+
144
168
  ## Consequences
145
169
 
146
170
  - The word `role` no longer names a goal membership in the API, the hall, or the docs, and in the DB it
@@ -151,5 +175,5 @@ removal, because until then the legacy field is load-bearing, not vestigial.
151
175
  the only new moving part on the write path, and it is the thing step 2 removes.
152
176
  - `docs/canonical-permissions.md` (the goal-management wall) now names `membership_kind`; the
153
177
  `authorizeGoalManagement` behaviour is untouched — this ADR changes no authority, floor, or grant.
154
- - The fifth use (`project_memberships.role`) shipped the same day under the amendment above — same shape, plus a cross-version window because its writer is another instance.
178
+ - The fifth use (`project_memberships.role`) shipped the same day under the amendment above — same shape, plus a cross-version window because its writer is another instance. That window closed under task 1003092 (`<redacted>`); both renames are now complete and one-spelling.
155
179
  - **No sixth use is known.** The remaining `role` spellings in the repo are the three legitimate ones ADR 0174 named: the crafts, `builders.system_role` (machine identity), and Discord's own noun — plus HTML's ARIA attribute, which every wall strips before matching.
@@ -54,6 +54,8 @@ The port and the address are therefore **one coupled decision**, and [task 10028
54
54
 
55
55
  The user-facing copy was corrected to match. It previously promised "the server goes away and **the address stops answering**" — untrue. It now says the address **stays reserved for this project and shows an error page until you bring it back**, which is both honest and accurate.
56
56
 
57
+ **Update (task 1002897).** Teardown now reclaims the address itself. It parks the hostname at the RFC 5737 blackhole through the same release a re-point uses ([ADR 0200](0200-re-point-a-bring-your-own-domain.md) §2: parked, not deleted), and it removes the `<slug>.caddy` block and reloads Caddy (`scripts/gds/provision-teardown.js`). Both steps are best-effort, and a platform-reserved hostname is never parked. `port` and `domain` are still held, so the address is inert without releasing the claim, and a revive's standup UPSERTs the same hostname straight back. The copy now says the address stays reserved and **nothing answers there until you bring it back**. The database and its dumps remain operator-only.
58
+
57
59
  ## Consequences
58
60
 
59
61
  - An owner can remove a project in any state from the manage page; the 409 `not_teardownable` class is gone except for the one honest case.
@@ -61,7 +61,7 @@ Today's box holds **11 of 15**. Resize (or open the next box) when it reaches **
61
61
 
62
62
  ### 4. The shapes: keep three, and add placement as its own question
63
63
 
64
- The measured cost is the same **unit** for `co-tenant` and `standalone`. What separates them is repository layout, not what they cost the box. So no new shape is needed. What the three shapes never answer is **which box**, and the rule is **standalone until the box reaches its ceiling, then the next box**. The box is the unit of spend, and this ceiling governs placement. Filed as [1003927](https://cloudbongos.com/builders#/task/1003927): the runner refuses to place a control-plane-local project past the ceiling.
64
+ The measured cost is the same **unit** for `co-tenant` and `standalone`. What separates them is repository layout, not what they cost the box. So no new shape is needed. What the three shapes never answer is **which box**, and the rule is **standalone until the box reaches its ceiling, then the next box**. The box is the unit of spend, and this ceiling governs placement. Filed as [1003927](https://cloudbongos.com/builders#/task/1003927): the runner refuses to place a control-plane-local project past the ceiling. The create route answers the same ceiling up front, as a 409 `box_full` before any row exists ([1003938](https://cloudbongos.com/builders#/task/1003938), [`capacity-gate.js`](../../modules/provisioning/capacity-gate.js)). The runner stays the wall, because only the runner can measure a remote box and the live memory floor.
65
65
 
66
66
  `dedicated` stays behind `provisioning.fleet.manage` ([`paid-shape-gate.js`](../../modules/provisioning/paid-shape-gate.js)). It has never been provisioned, and it should not be offered to anyone paying nothing: at $6/mo minimum ($24 at the runner's default tier), one dedicated project costs **8–40× a shared slot**.
67
67
 
@@ -0,0 +1,33 @@
1
+ # 0288 — The goal wall has a shared floor for paths no module owns
2
+
3
+ **Status:** accepted · 2026-09-16 · [task 1003655](https://cloudbongos.com/builders#/task/1003655)
4
+
5
+ ## Context
6
+
7
+ [ADR 0086](0086-goal-scoped-work-hierarchy.md) §3 gives a goal member bounded authority to author their own tasks: every element of a candidate task's `touches[]` must be *contained within* the union of the goal's modules' globs, resolved through `MODULE_SCOPE_MAP`. The containment is directional (`matchOne`, not the symmetric `touchesOverlap`) precisely so the wall cannot be widened by declaring a broad parent path.
8
+
9
+ That map answers one question — *who owns this path* — and it answers it for the shipping surfaces. `tests/` has no honest answer. It is 751 flat suites named by feature rather than by module, covering all ~24 keys, and no key claims it. `scopeSubset()` therefore denied every touch under it, under every goal.
10
+
11
+ Since every real task adds a test, the consequence was structural: `POST /goals/:id/tasks` refused any task that honestly declared its own test file, whatever goal it was filed under. The gate only looked passable because callers under-declared their `touches[]`. This was filed as idea 1000816 while checking coverage for unrelated work; it had not yet surfaced as anyone's confusing 403 because no live caller sent `touches` to the capture-time routing path added in R05 ([task 1003066](https://cloudbongos.com/builders#/task/1003066)).
12
+
13
+ The same idea named `.claude/skills/` as a second instance. That half resolved itself: [task 1002989](https://cloudbongos.com/builders#/task/1002989) gave the methodology key `.claude/skills/` as the floor under the ~50 skills no module ships. Only `tests/` remained.
14
+
15
+ ## Decision
16
+
17
+ **`MODULE_SCOPE_MAP` gains `SHARED_GLOBS` — a small set of globs inside every goal's wall and outside every module's territory. Its sole entry is `tests/`.**
18
+
19
+ The bar for joining it is deliberately narrow, and both halves must hold: **no module can honestly own the path, and every task legitimately writes it.**
20
+
21
+ `goal-scope-check.scopeSubset()` treats a touch as covered if it is contained in the goal's module globs **or** in the shared floor. The floor is reported as its own `shared_globs` field and is never folded into `goal_globs`, because `check()`'s `unknown_scope` guard tests `goal_globs.length === 0` — mixing an always-present list in would give a goal scoped to nothing but garbage a non-empty territory and slip it past that fail-closed edge.
22
+
23
+ **The exported floor is derived, not trusted.** On the same contract that makes `protected` a derivation rather than a literal (ADR 0086 §7, the SEC #863 second-list failure mode), a raw entry that overlaps `PROTECTED_GLOBS` is filtered out of the export. A protected path listed here would widen every goal's wall into the trust core in a single line.
24
+
25
+ ## Consequences
26
+
27
+ - **A task can declare its own test honestly.** The gate stops rewarding under-declaration, which is what made the wall's numbers wrong rather than merely strict.
28
+ - **The other two walls are untouched.** Claim collisions are still caught by `touchesOverlap` on `touches[]`, and the protected-path wall runs independently of the subset wall — a sub-Metic author is still blocked from the trust core whether or not a path is shared. `tests/` is not in `PROTECTED_GLOBS`, so the floor opens nothing that was closed.
29
+ - **A test file is not a shipping surface.** Nothing imports `tests/*.mjs`, so a member authoring one has zero blast radius on another module's code. That is what makes shared territory sound here and would not make it sound for, say, `src/`.
30
+ - **Module-owned suites stay owned.** `modules/<key>/tests/` is already inside its module's glob and is not part of the floor.
31
+ - Rejected — **giving `tests/` to a key** (`devsecops` or `methodology`). It is a false ownership claim: an ideas-module test is the ideas module's work. It would also hand one goal's members the entire suite while denying it to everyone else, which is worse than the gap it closes.
32
+ - Rejected — **enumerating per-module test paths.** The suites are named by feature, not module, and the glob format has no wildcards to carve with, so the list would rot on the next test file.
33
+ - **Adding a second entry is a design decision, not housekeeping.** The floor is the one place where a line of data widens every wall at once, so it carries the derivation guard and a unit test asserting it stays narrow.
@@ -0,0 +1,33 @@
1
+ # 0289 — An unconsumed contribution is a CI failure, not a boot refusal
2
+
3
+ **Status:** accepted · 2026-09-16 · [task 1003819](https://cloudbongos.com/builders#/task/1003819) (successor to abandoned task 1002464)
4
+
5
+ ## Context
6
+
7
+ A module declares what it contributes in `module.json`. Two checks read that block and neither asks the question that matters here:
8
+
9
+ - `src/module-seams.js` `verifyPortsSatisfied` checks the **opposite** direction — every `consumes` port has exactly one provider among the enabled modules. It says nothing about a `contributes` entry with no consumer.
10
+ - `src/module-loader/manifest-schema.js` `validateManifest` checks the **shape** of `contributes` — a closed key set, so a typo'd *key* errors. It does not look at whether the value resolves to anything.
11
+
12
+ So a module can declare `contributes.pollers: ["ghost"]` with no `modules/<key>/pollers/ghost.js` and boot stays silent. That silence is not a bug in the consumers: `startModulePollers` skips a missing poller file because a real poller may run on the control plane rather than in this process, `hallWidgetScripts` skips a `uiSections` entry with no `hall-widget.js` because a section can be declarative, and `moduleWebSurfaces` skips a surface whose directory was never shipped. Each skip is correct for a genuine declarative-only entry, and **indistinguishable from a typo or a rename that left the manifest behind**.
13
+
14
+ Verified on `main` 2026-09-16: 84 declared seam entries across 21 modules resolve to a real artifact, and five do not — `dev-box`'s `box-lifecycle` poller and `boxes` uiSection (both genuinely declarative), and three `ui-design` skills that live in the core `.claude/skills/` rather than the module tree, where `moduleSkillSources` silently skips them. Nothing was broken; nothing could tell you that.
15
+
16
+ ## Decision
17
+
18
+ **A declared `contributes.<seam>` whose consumer's artifact is absent fails CI — `scripts/gds/seam-wiring-guard.js`, fitness Check 33 — and does not affect boot.**
19
+
20
+ The guard's `SEAM_CONSUMERS` table pairs each seam with **the function that actually reads it** and the artifact that function resolves, so the check asks the same question the consumer asks rather than inventing a second convention that could drift from it. A seam with no reader anywhere (`rooms`, allowed by the schema since the doorway but wired to nothing) carries `consumer: null`, and every declaration of it is reported.
21
+
22
+ **The exemption is per-entry and lives in the manifest**: `declarativeSeams: { "pollers": ["box-lifecycle"] }`. It is validated in `validateManifest`, against the module's own `contributes`, because that is the one place holding both — so an exemption naming a seam the module does not declare, or an entry it does not list, is an **error**. A typo'd exemption that validated clean would exempt nothing while reading as though it exempted something, which is the failure direction that hides a defect. The guard adds the mirror case: an exemption whose artifact **now exists** is reported as stale, so the list cannot quietly become a lie in either direction.
23
+
24
+ **The wired set is reported on the pass path**, not only the failure path — 84 wired, 5 declarative-only — because a check that is silent when it passes gives no evidence it ran.
25
+
26
+ ## Consequences
27
+
28
+ - **The manifest stops being able to describe a feature that isn't there.** A rename that leaves `contributes` behind now reddens the build instead of silently shipping a module one feature short.
29
+ - **CI, not boot, and the asymmetry with `verifyEnabledPorts` is deliberate.** That check throws at boot because an unprovided port otherwise surfaces as a `resolve()` throw on a request path, in a process that already reported healthy — refusing early is strictly safer. For every seam but `routes`, an unconsumed contribution is not that shape: the consumer skips it gracefully, nothing crashes later, and the instance runs correctly minus one feature. Adding a production start-up failure for a manifest-hygiene defect would trade a real outage risk for a cosmetic one, and a red build costs nobody uptime.
30
+ - **`routes` is the exception, and it argues the same way.** `mountModuleRoutes` `require`s the factory with no try/catch, so a missing route file already takes the boot down today — pre-existing, and not changed here. For that seam the guard is not a new gate: it moves an existing crash from production start-up to the build. Strictly earlier, strictly cheaper. CI therefore covers both shapes — the seams that fail silently get a voice, and the one that fails loudly gets to fail before it reaches a server.
31
+ - **A new `contributes` key must be added to `SEAM_CONSUMERS` too**, or it is silently unchecked — the same gap in a new place. A test asserts the guard spans every key `ALLOWED_CONTRIBUTES` permits, so forgetting fails CI rather than going unnoticed.
32
+ - **`rooms` is now visibly dead.** The schema has allowed it since the doorway and nothing ever read it. No module declares it, so nothing fails today; the first one that does gets told.
33
+ - Rejected — **a boot-time refusal beside `verifyPortsSatisfied`**: see the asymmetry above. Rejected — **a blanket skip for the seams that legitimately allow declarative entries** (`pollers`, `uiSections`): that is how the gap arose, and it would hide the real cases the guard exists to catch. Rejected — **rebasing branch `task-1002464`**: 333 lines, 461 main commits behind its base, against a `loader.js` since rewritten. Its two test files were read for the property checklist; PR [#589](https://github.com/example-owner/cloud-bongos/pull/589) should be closed rather than revived.
@@ -0,0 +1,40 @@
1
+ # 0290 — "Disconnect from Bongos entirely" is the owner-initiated exception to retention
2
+
3
+ **Status:** accepted · 2026-09-16 · [task 1003249](https://cloudbongos.com/builders#/task/1003249)
4
+
5
+ ## Context
6
+
7
+ [ADR 0181](0181-teardown-state-gate-and-address-retention.md) made retention deliberate. A teardown keeps the project's database, its nightly dumps, its `port` and its `domain`, so "Take it offline" is always reversible and a stale proxy block can never hand one owner's address to another project. [Task 1002897](https://cloudbongos.com/builders#/task/1002897) then made the kept address inert: parked at 192.0.2.1, with no Caddy block.
8
+
9
+ That left owners with no way to actually be done with a project. The manage page admitted it: "There is no permanent-delete button — clearing a project's database for good, and freeing its web address for another project, are done by hand today." The owner decided on 2026-08-24 (walkthrough decision 6) to rename the teardown control "Take it offline" and to add a separate, true delete.
10
+
11
+ ## Decision
12
+
13
+ **1. A separate action, owner-initiated only.** `POST /provisioning/instances/:id/disconnect` is own-scoped with the archon override (404 for anyone else) and carries the typed name as `confirm_slug`, which the route checks server-side, because this erases a database. It is its own card after "Take it offline", never a mode of it, so the reversible control can never read as the destructive one. Every other path still retains everything, as ADR 0181 says.
14
+
15
+ **2. It deletes the provisioning row** (owner decision, this session). The alternative, a tombstone, would keep the name taken forever, since `slug` is UNIQUE. Deleting the row releases all three claims at once: the name, the port (`allocatePort` reads every row) and the address (the next standup UPSERTs it). The row's intents, events and OAuth-manifest attempts cascade with it. The core `audit_log` still records the request, the route writes a `disconnect` event before the delete, and the runner journal records each step.
16
+
17
+ **3. It erases the backups immediately** (owner decision, this session). There is no per-project retention mechanism to hold a "last dump for 30 days", and building one would add a scheduler to operate. The safety net is the typed-name confirm plus a fold that says "cannot be undone". The erase matches the nightly filename exactly (`<db>-YYYY-MM-DDTHHMMSSZ.sql.gz`). The nightly prune's own `<db>-*` glob would let a project named `test` erase `test-nk`'s dumps.
18
+
19
+ **4. The runner does it, in an order that keeps it retryable.** `scripts/gds/provision-disconnect.js`, dispatched from the drain loop's `disconnect` intent (migration `provisioning_016`), runs these steps in order:
20
+ - the teardown legs;
21
+ - dropping the database, then the role that owns it;
22
+ - erasing the dumps;
23
+ - removing the env and state dirs, the per-project checkout (standalone only) and the unix account;
24
+ - removing the catalog, membership and client rows through the new `project.catalog` verb `forget`, under the publish path's two claim rules;
25
+ - **deleting the row last**.
26
+
27
+ A failed erase or catalog step returns a retryable `{ ok:false }`; a teardown that throws ends the run with the intent at `error`, before anything is erased. Either way the row stays, because the row is what lets the runner, or the owner pressing Disconnect again, try again. A dedicated project skips the command erase, since its droplet went whole. The slug is re-validated before any `rm -rf` is built from it.
28
+
29
+ **5. A disconnect supersedes nothing.** Unlike a teardown, which supersedes a stale pending standup, a disconnect is refused while any other run is open, even a pending one. A destructive run must never race a standup that is about to start. A repeat while a disconnect is open is idempotent.
30
+
31
+ **6. The GitHub repository is never touched**, and neither is a GitHub App the owner registered for sign-in. GitHub has no API to delete an App, so the fold tells the owner where to remove it.
32
+
33
+ ## Consequences
34
+
35
+ - The manage page's copy changed with the capability. The teardown truth now ends "Taking it offline erases nothing." rather than "Nothing here erases a project.", and the fold points at the new card instead of admitting the capability is missing.
36
+ - The `project.catalog` port has three verbs. `tests/catalog_only_client.mjs` still pins the list.
37
+ - If the runner dies mid-disconnect, the reconcile sweep re-enqueues a *teardown* for a `tearing_down` row, not a disconnect. The project is left offline, with its row, and the owner presses Disconnect again. This is accepted rather than giving the reconcile a memory of intent.
38
+ - The nightly prune's `<db>-*.sql.gz` glob can still reach a longer-named sibling's dumps older than 30 days. It is filed as its own row, not fixed here.
39
+
40
+ Proofs: `tests/provisioning_disconnect.mjs` (queue + route), `tests/provision_disconnect.mjs` (runner order, shapes, failure, guards), `tests/project_catalog_forget.mjs` (claim rules), `tests/hub_disconnect_ui.mjs` + the kit probe (the card).
@@ -379,3 +379,6 @@ This keeps the decision history honest and traceable.
379
379
  | 0285 | [**A shared box holds about twelve projects per GB, and memory is the wall** ([task 1003877](https://cloudbongos.com/builders#/task/1003877) · goal 1000106, a spike measured read-only on the live control plane at the owner's call). A project costs ~80 MB warm (70 MB Node + one busy Postgres backend); idle projects look like ~25 MB only because swap holds their cold pages, and swap is not capacity. Fixed base ~750 MB on a control plane. Ceiling per tier `floor((usable − fixed) / 80)`: 15 on today's `s-1vcpu-2gb` (11 live), 39 on `s-2vcpu-4gb`; resize at 12. RAM binds first, Postgres connections (pool default 10 × N vs 97) second; CPU, ports, units, disk and the backup window do not bind. Keep the three shapes, add placement ("standalone until the box is full, then the next box"); a shared slot costs box price ÷ ceiling ($0.62–0.80/mo), `dedicated` 8–40× that.](0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md) | infra / Cloud Bongos |
380
380
  | 0286 | [**A gated project's manifest tells a stranger how to sign in, and nothing else** ([task 1003652](https://cloudbongos.com/builders#/task/1003652) · goal 1000106, from idea 1000845; closes ADR 0192's deferred item). `GET /instance` stays exempt from the member door, but on a gated project a caller with no live session gets `{ schema, name, brand: { identity.productName, project }, auth (incl. idp), admission }` — no modules, ranks, repo coordinates, dev-box flag or branding strings. Members get the full manifest; a failed lookup answers as a stranger; gated responses are `private, no-store` + `Vary: Cookie, Authorization`. Schema id stays `instance-manifest/v1` (a subset; `bongos login` refuses any other). Owner's call: sign-in basics only.](0286-a-gated-project-s-manifest-tells-a-stranger-how-to-sign-in-and-nothing-else.md) | security / Cloud Bongos |
381
381
  | 0287 | [**A backlog task that nothing gates promotes itself** ([task 1003932](https://cloudbongos.com/builders#/task/1003932), owner decision 2026-09-15). Every promotion path in migration 163 is an `AFTER UPDATE` trigger at the DEPENDENCY end of an edge, so two states are unreachable by all of them: a task with no gating edge is nobody's dependent and is never evaluated, and an edge added AFTER its dependency shipped missed the only event that would have fired. Measured live: **155 backlog tasks promotable and parked**, 33 of 41 in one goal. Same failure `runCriterionSweep` exists for on the criterion axis (ADR 0183) — a reactive rule cannot drain rows whose event has passed. **Decision: `runPromotionSweep` joins the publish-reconciler tick** and promotes any backlog task with no unmet dependency and no open blocker. The owner chose the widest rule — spikes (excluded by 163 on purpose) and cascade-filed artist reviews included, no carve-outs; promoting only makes a task claimable, it compels nobody. **It ships DISARMED**, reporting what it would promote until `BONGOS_PROMOTION_SWEEP_ARMED=1`, because ~155 rows changing status at once deserves a preview. The **open-blocker guard is re-checked in the sweep's own SQL**: migration 176 folded that clause into `task_is_fully_unblocked` and migration 178 dropped it while claiming to restore "the 163 body" — verified against production, where one task already sits at `ready` with an open blocker (its fix is task 1003934). A bulk promoter must not inherit a single point of failure that has been reverted once. Consequence the owner accepted: **`backlog` stops being a resting state** and `/backlog-review` loses most of its purpose. Rejected: promoting at creation (breaks `cascade.js`'s tested backlog invariant and fixes nothing already parked); scoping the first run to one goal (leaves the defect live in ten others).](0287-a-backlog-task-that-nothing-gates-promotes-itself.md) | lifecycle / work tracking |
382
+ | 0288 | [**The goal wall has a shared floor for paths no module owns** ([task 1003655](https://cloudbongos.com/builders#/task/1003655), from idea 1000816). `MODULE_SCOPE_MAP` answers "who OWNS this path", and `tests/` has no honest answer — 751 flat suites named by feature, covering all ~24 keys, claimed by none. So ADR 0086 §3's directional subset check denied every touch under it, under every goal: `POST /goals/:id/tasks` refused any task that honestly declared its own test file, and the gate only looked passable because callers under-declared their `touches[]`. (The idea's second instance, `.claude/skills/`, was already resolved by task 1002989's methodology key.) **Decision: `SHARED_GLOBS` — globs inside EVERY goal's wall and outside every module's territory, sole entry `tests/`.** The bar is narrow and both halves must hold: no module can honestly own it AND every task legitimately writes it. It is reported as its own `shared_globs` and **never folded into `goal_globs`**, because `check()`'s `unknown_scope` guard tests `goal_globs.length === 0` — folding an always-present list in would give a garbage scope a non-empty territory and slip it past that fail-closed edge. The export is **derived, not trusted**: a raw entry overlapping `PROTECTED_GLOBS` is filtered out, on the same ADR 0086 §7 contract that makes `protected` a derivation (one line here would otherwise widen every wall into the trust core). Sound because a test file is not a shipping surface — nothing imports it, so blast radius on another module is zero — and the claim-collision and protected-path walls are untouched. Rejected: giving `tests/` to `devsecops`/`methodology` (false ownership, and it hands one goal the whole suite while denying everyone else); enumerating per-module test paths (the glob format has no wildcards, so the list rots on the next test file).](0288-the-goal-wall-has-a-shared-floor-for-paths-no-module-owns.md) | lifecycle / work tracking |
383
+ | 0289 | [**An unconsumed contribution is a CI failure, not a boot refusal** ([task 1003819](https://cloudbongos.com/builders#/task/1003819), successor to abandoned task 1002464). A module declares `contributes` and nothing checked that a declared seam is actually WIRED: `verifyPortsSatisfied` asks the OPPOSITE question (every `consumes` has exactly one provider) and `validateManifest` checks only the SHAPE (a closed key set, so a typo'd KEY errors). So `contributes.pollers: ["ghost"]` with no `pollers/ghost.js` boots silent — and correctly so, because `startModulePollers`, `hallWidgetScripts` and `moduleWebSurfaces` each skip a missing artifact the same way they skip a genuine declarative-only entry. Verified on main: 84 declared entries resolve, 5 do not. **Decision: `scripts/gds/seam-wiring-guard.js`, fitness Check 33.** Its `SEAM_CONSUMERS` table pairs each seam with THE FUNCTION THAT ACTUALLY READS IT, so the guard asks the consumer's own question instead of a second convention that could drift; a seam with no reader anywhere (`rooms`) carries `consumer: null` and every declaration of it is reported. The exemption is **per-entry, in the manifest** (`declarativeSeams`), validated in `validateManifest` against the module's own `contributes` — a typo'd exemption is an ERROR, since one that validated clean would exempt nothing while reading as though it exempted something — and a **stale** exemption whose artifact now exists is reported too, so the list cannot lie in either direction. The **wired set prints on the PASS path**: a check that is silent when it passes is no evidence it ran. CI and not boot, deliberately asymmetric with `verifyEnabledPorts`: an unprovided port becomes a runtime throw on a request path in a healthy-looking process, while an unconsumed contribution crashes nothing — a start-up refusal would trade a real outage risk for a cosmetic one. Rejected: a boot refusal; a blanket skip for the seams that allow declarative entries (that is how the gap arose); rebasing branch `task-1002464` (333 lines, 461 commits behind, against a since-rewritten `loader.js` — read its tests, close PR #589).](0289-an-unconsumed-contribution-is-a-ci-failure-not-a-boot-refusal.md) | modules / architecture |
384
+ | 0290 | [**"Disconnect from Bongos entirely" is the owner-initiated exception to retention** ([task 1003249](https://cloudbongos.com/builders#/task/1003249), owner decision 2026-08-24 + this session). ADR 0181 keeps a torn-down project's database, dumps, port and address so "Take it offline" is reversible; this is the separate true delete. Own-scoped + archon override, with the typed name checked SERVER-side (`confirm_slug`); refused while any other run is open (supersedes nothing). The runner tears down, drops the database and its role, erases the dumps by their EXACT nightly filename, removes env/state/checkout/account, forgets the catalog + membership + client rows (new `project.catalog` verb `forget`, both claim rules), and deletes the provisioning row LAST — releasing name, port and address, and keeping every earlier failure retryable. The GitHub repository and any sign-in App are never touched.](0290-disconnect-is-the-owner-initiated-exception-to-address-retention.md) |
@@ -9833,7 +9833,7 @@
9833
9833
  "me"
9834
9834
  ],
9835
9835
  "summary": "GET /me/render",
9836
- "description": "Per-builder \"render widgets\" knob (task 1279, shares the behavior_prefs jsonb / migration 119). Whether the deterministic session cards render as a rich in-feed HTML widget or the plain text fallback. The widget is nicer but the agent must relay its full markup verbatim into show_widget (read + re-emit) every session-start / claim / ship stage — a real token cost. This toggle turns that off; when off the CLIs emit only text (no HTML relayed). No rank cap (pure cost/taste, not a trust gate); default ON. render-prefs.js is the single source of truth. rank: any-builder — own profile config; mirrors the skill/sound/wandering routes.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
9836
+ "description": "Per-builder CARD RICHNESS knob (task 1279, a four-rung ladder since task 1002518; shares the behavior_prefs jsonb / migration 119). How much the deterministic session cards spend on the reader: nothing but the guaranteed confirmation line, the plain text card, the in-feed HTML widget, or that widget with its summary already open. The widget is nicer but the agent must relay its full markup verbatim into show_widget (read + re-emit) every session-start / claim / ship stage — a real token cost. No rank cap (pure cost/taste, not a trust gate). The DEFAULT is derived from the builder's interaction profile rather than being a constant, so a builder who never opens this panel still gets a card pitched at the profile they did set. render-prefs.js is the single source of truth for both the ladder and the derivation. rank: any-builder — own profile config; mirrors the skill/sound/wandering routes.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
9837
9837
  "x-rank": "any-builder",
9838
9838
  "x-source": "modules/builder-settings/routes/settings.js",
9839
9839
  "responses": {
@@ -9862,7 +9862,7 @@
9862
9862
  "me"
9863
9863
  ],
9864
9864
  "summary": "PATCH /me/render",
9865
- "description": "PATCH the render knob. Body: { widgets: <bool> | null } where null clears the override (fall back to the default ON). Any other key, or a non-boolean value → 422. Empty body is a no-op (returns current). Sibling behavior keys (e.g. wandering) are preserved by applyPatch. rank: any-builder — self-service profile edit; same posture as /me/wandering.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
9865
+ "description": "PATCH the render knob. Body: { widgets: <level> | <bool> | null } where null clears the override (fall back to the DERIVED level). The legacy boolean is still accepted and normalised to the rung it always meant, so an older hall build keeps working against this core. Any other key, or a value that is neither a known level nor a boolean nor null → 422. Empty body is a no-op (returns current). Sibling behavior keys (wandering, interaction) are preserved by applyPatch. rank: any-builder — self-service profile edit; same posture as /me/wandering.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
9866
9866
  "x-rank": "any-builder",
9867
9867
  "x-source": "modules/builder-settings/routes/settings.js",
9868
9868
  "requestBody": {
@@ -12191,7 +12191,7 @@
12191
12191
  "provisioning"
12192
12192
  ],
12193
12193
  "summary": "POST /provisioning/instances",
12194
- "description": "POST /provisioning/instances — request a new instance. rank: any authenticated builder (own resource); a `dedicated` (metered) shape additionally requires provisioning.fleet.manage — see requireAuthorityForMeteredShape above. ENQUEUES a 'provision' intent + records the request; the runner (task P3) does the real standup. Idempotent on slug for the SAME owner (re-request returns the existing row); a slug owned by ANOTHER builder → 409. Body: { slug (required), target_ref?, hosting_shape? (co-tenant|dedicated), domain?, tier?, onboard_mode? (greenfield|adopt) }. onboard_mode is LOAD-BEARING, not a label (task 1002562): the control-plane runner branches its repo scaffold on it. 'adopt' means target_ref is a repo the owner ALREADY has, so the runner must clone + layer onto its history instead of pushing a fresh tree at `main` (which an existing repo rejects as unrelated history). Absent → 'greenfield', the safe default.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
12194
+ "description": "POST /provisioning/instances — request a new instance. rank: any authenticated builder (own resource); a `dedicated` (metered) shape also needs provisioning.fleet.manage (../paid-shape-gate.js), and a full shared box answers 409 box_full up front (../capacity-gate.js). ENQUEUES a 'provision' intent the runner (task P3) stands up. Idempotent on slug for the SAME owner (re-request returns the existing row); a slug owned by ANOTHER builder → 409. Body: { slug (required), target_ref?, hosting_shape? (co-tenant|dedicated), domain?, tier?, onboard_mode? (greenfield|adopt) }. onboard_mode is LOAD-BEARING, not a label (task 1002562): the control-plane runner branches its repo scaffold on it. 'adopt' means target_ref is a repo the owner ALREADY has, so the runner must clone + layer onto its history instead of pushing a fresh tree at `main` (which an existing repo rejects as unrelated history). Absent → 'greenfield', the safe default.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
12195
12195
  "x-rank": "any-builder",
12196
12196
  "x-source": "modules/provisioning/routes/provisioning.js",
12197
12197
  "requestBody": {
@@ -12341,6 +12341,69 @@
12341
12341
  ]
12342
12342
  }
12343
12343
  },
12344
+ "/provisioning/instances/{id}/disconnect": {
12345
+ "post": {
12346
+ "operationId": "post_provisioning_instances_id_disconnect",
12347
+ "tags": [
12348
+ "provisioning"
12349
+ ],
12350
+ "summary": "POST /provisioning/instances/:id/disconnect",
12351
+ "description": "POST /provisioning/instances/:id/disconnect — rank: any authenticated builder (own resource); an archon may disconnect any project. 404 for anyone else, so another owner's id is never confirmed. The typed name is checked HERE, not only in the page: this erases a database, so a replayed or scripted request must carry the same proof the button does. No status gate — any state may be disconnected; the queue refuses while another run is open (disconnect.enqueueDisconnect).\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
12352
+ "x-rank": "any-builder",
12353
+ "x-source": "modules/provisioning/routes/disconnect.js",
12354
+ "parameters": [
12355
+ {
12356
+ "name": "id",
12357
+ "in": "path",
12358
+ "required": true,
12359
+ "schema": {
12360
+ "type": "string"
12361
+ },
12362
+ "description": "Path parameter `id`."
12363
+ }
12364
+ ],
12365
+ "requestBody": {
12366
+ "required": true,
12367
+ "content": {
12368
+ "application/json": {
12369
+ "schema": {
12370
+ "$ref": "#/components/schemas/PostProvisioningInstancesIdDisconnectRequest"
12371
+ }
12372
+ }
12373
+ },
12374
+ "x-validated": true
12375
+ },
12376
+ "responses": {
12377
+ "200": {
12378
+ "description": "Success.",
12379
+ "content": {
12380
+ "application/json": {
12381
+ "schema": {
12382
+ "$ref": "#/components/schemas/PostProvisioningInstancesIdDisconnectResponse"
12383
+ }
12384
+ }
12385
+ }
12386
+ },
12387
+ "400": {
12388
+ "$ref": "#/components/responses/ValidationFailed"
12389
+ },
12390
+ "401": {
12391
+ "$ref": "#/components/responses/Unauthorized"
12392
+ },
12393
+ "403": {
12394
+ "$ref": "#/components/responses/Forbidden"
12395
+ },
12396
+ "404": {
12397
+ "$ref": "#/components/responses/NotFound"
12398
+ }
12399
+ },
12400
+ "security": [
12401
+ {
12402
+ "builderSession": []
12403
+ }
12404
+ ]
12405
+ }
12406
+ },
12344
12407
  "/provisioning/instances/{id}/domain": {
12345
12408
  "post": {
12346
12409
  "operationId": "post_provisioning_instances_id_domain",
@@ -14620,7 +14683,7 @@
14620
14683
  "sso"
14621
14684
  ],
14622
14685
  "summary": "POST /sso/membership/check-in",
14623
- "description": "POST /sso/membership/check-in — a federated instance reports, on every sign-in, that a builder builds on its project (ADR 0141 §3). Server-to-server, authed by client_id + client_secret (same constant-time check as /sso/token). Refreshes an EXISTING `platform_identity_project_memberships` row's COARSE membership_kind. The client_id is taken from the AUTHENTICATED client — the caller cannot report membership for a project it doesn't hold the secret for. WHAT THIS CALL MAY NOT DO, AND WHY (ADR 0205, security report 1000027). Client credentials say WHICH PROJECT is calling; they say nothing about WHO the github_id in the body is. So this route may neither create a membership nor rewrite an account's identity: it updates a row the HUB already witnessed a sign-in for (signin_verified_at, stamped only at /sso/token and /sso/device/poll), and it creates the account shell only when absent. Before this, a client holding only its own secret could overwrite anyone's public github_login + avatar_url AND mint the membership that /sso/activity/rollup's membership_required precheck (the ADR 0171 D7 anti-impersonation gate) trusts — so the gate was satisfiable by the very call it was meant to gate. Nothing legitimate is lost: check-in only ever fires after a hub-authenticated sign-in (src/bongos/auth-idp.js), which stamps the witness first. THE FIELD IS `membership_kind`; `role` IS ACCEPTED FOR THE WINDOW (task 1003090, ADR 0177 §\"the fifth use\"). ADR 0174 reserves `role` for the three crafts, and the old value 'builder' was a craft name meaning \"non-owner member\" — so both the field and that value are renamed. Unlike the goal-membership rename, this is a CROSS-VERSION wire contract: the callers are other instances on their own upgrade schedule, so an un-upgraded instance still sends `role: 'builder'` and the hub must keep understanding it. `membership_kind` WINS when both arrive (a sender in the window sends both deliberately). rank: public — authenticated by client_id + client_secret, not a builder rank.\n\n**Rank:** `public` — No authentication — any caller.",
14686
+ "description": "POST /sso/membership/check-in — a federated instance reports, on every sign-in, that a builder builds on its project (ADR 0141 §3). Server-to-server, authed by client_id + client_secret (same constant-time check as /sso/token). Refreshes an EXISTING `platform_identity_project_memberships` row's COARSE membership_kind. The client_id is taken from the AUTHENTICATED client — the caller cannot report membership for a project it doesn't hold the secret for. WHAT THIS CALL MAY NOT DO, AND WHY (ADR 0205, security report 1000027). Client credentials say WHICH PROJECT is calling; they say nothing about WHO the github_id in the body is. So this route may neither create a membership nor rewrite an account's identity: it updates a row the HUB already witnessed a sign-in for (signin_verified_at, stamped only at /sso/token and /sso/device/poll), and it creates the account shell only when absent. Before this, a client holding only its own secret could overwrite anyone's public github_login + avatar_url AND mint the membership that /sso/activity/rollup's membership_required precheck (the ADR 0171 D7 anti-impersonation gate) trusts — so the gate was satisfiable by the very call it was meant to gate. Nothing legitimate is lost: check-in only ever fires after a hub-authenticated sign-in (src/bongos/auth-idp.js), which stamps the witness first. THE FIELD IS `membership_kind` — AND ONLY THAT (task 1003090 renamed it, task 1003092 closed the window; ADR 0177 §\"the fifth use\"). ADR 0174 reserves `role` for the three crafts, and the old value 'builder' was a craft name meaning \"non-owner member\" — so both the field and that value were renamed. Because the callers are OTHER instances on their own upgrade schedule, the hub kept accepting the legacy field for a cross-version window; that window closed once every federated instance that can check in was past core 1.19.241. The schema below is strict, so a legacy-only body is refused with 400 and writes nothing — tests/sso_checkin_legacy_field_gone.mjs is the test of its absence. rank: public — authenticated by client_id + client_secret, not a builder rank.\n\n**Rank:** `public` — No authentication — any caller.",
14624
14687
  "x-rank": "public",
14625
14688
  "x-source": "modules/platform-identity/routes/sso.js",
14626
14689
  "requestBody": {
@@ -22169,6 +22232,38 @@
22169
22232
  "project"
22170
22233
  ]
22171
22234
  },
22235
+ "PostProvisioningInstancesIdDisconnectRequest": {
22236
+ "type": "object",
22237
+ "properties": {
22238
+ "confirm_slug": {
22239
+ "type": "string",
22240
+ "maxLength": 40
22241
+ }
22242
+ },
22243
+ "required": [
22244
+ "confirm_slug"
22245
+ ],
22246
+ "additionalProperties": false
22247
+ },
22248
+ "PostProvisioningInstancesIdDisconnectResponse": {
22249
+ "type": "object",
22250
+ "properties": {
22251
+ "ok": {
22252
+ "type": "boolean"
22253
+ },
22254
+ "queued": {},
22255
+ "action": {},
22256
+ "status": {},
22257
+ "message": {}
22258
+ },
22259
+ "required": [
22260
+ "ok",
22261
+ "queued",
22262
+ "action",
22263
+ "status",
22264
+ "message"
22265
+ ]
22266
+ },
22172
22267
  "PostProvisioningInstancesIdDomainRequest": {
22173
22268
  "type": "object",
22174
22269
  "properties": {
@@ -22823,10 +22918,6 @@
22823
22918
  "membership_kind": {
22824
22919
  "type": "string",
22825
22920
  "maxLength": 16
22826
- },
22827
- "role": {
22828
- "type": "string",
22829
- "maxLength": 16
22830
22921
  }
22831
22922
  },
22832
22923
  "required": [
@@ -23688,9 +23779,9 @@
23688
23779
  "description": "A required dependency/feature is not configured or is temporarily down."
23689
23780
  }
23690
23781
  },
23691
- "x-endpoint-count": 373,
23692
- "x-schema-count": 412,
23782
+ "x-endpoint-count": 374,
23783
+ "x-schema-count": 414,
23693
23784
  "x-undocumented-bodies": 9,
23694
- "x-response-schemas": 280,
23785
+ "x-response-schemas": 281,
23695
23786
  "x-generated-by": "scripts/gds/gen-api-docs.js"
23696
23787
  }
@@ -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. 373 endpoints across 60 route files.
5
+ > **Generated from the live route files** — the route file is authoritative. 374 endpoints across 61 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`.
@@ -401,7 +401,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
401
401
  | GET | `/api/bongos/me/profile-nudge` | `any-builder` | — | The "Complete your profile" nudge dismissal (task 1002802 / P06). |
402
402
  | PATCH | `/api/bongos/me/profile-nudge` | `any-builder` | _undocumented_ | PATCH the dismissal. |
403
403
  | GET | `/api/bongos/me/recent-ships` | `any-builder` | — | Tasks this builder has shipped, most recent first. |
404
- | GET | `/api/bongos/me/render` | `any-builder` | — | Per-builder "render widgets" knob (task 1279, shares the behavior_prefs jsonb / migration 119). |
404
+ | GET | `/api/bongos/me/render` | `any-builder` | — | Per-builder CARD RICHNESS knob (task 1279, a four-rung ladder since task 1002518; shares the behavior_prefs jsonb / migration 119). |
405
405
  | PATCH | `/api/bongos/me/render` | `any-builder` | _undocumented_ | PATCH the render knob. |
406
406
  | GET | `/api/bongos/me/sessions` | `any-builder` | — | GET /api/bongos/me/sessions — every live session on the CALLER's account. |
407
407
  | POST | `/api/bongos/me/sessions/revoke` | `any-builder` | `session_id` | POST /api/bongos/me/sessions/revoke — kill ONE of the caller's own sessions. |
@@ -488,7 +488,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
488
488
  | GET | `/api/bongos/projects/featured` | `public` | — | the public map of projects; no auth by design. |
489
489
  | POST | `/api/bongos/projects/invite` | `metic+archon` | `client_id`, `github_login` | own-or-metic in effect: invite a builder into a project (ADR 0141 §5, task #1002115), called by a `project.curate` holder OR the project'… |
490
490
 
491
- ## `provisioning` (22)
491
+ ## `provisioning` (23)
492
492
 
493
493
  | Method | Path | Rank | Body | Description |
494
494
  |---|---|---|---|---|
@@ -501,6 +501,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
501
501
  | POST | `/api/bongos/provisioning/instances` | `any-builder` | `slug`, `target_ref`, `hosting_shape`, `domain`, `no_address`, `tier`, `onboard_mode`, `type`, `description`, `detail`, `modules` | POST /provisioning/instances — request a new instance. |
502
502
  | GET | `/api/bongos/provisioning/instances/:id` | `any-builder` | — | GET /provisioning/instances/:id — read one instance (the manage page's read). |
503
503
  | PATCH | `/api/bongos/provisioning/instances/:id/detail` | `any-builder` | `description`, `detail`, `modules`, `type` | ── The optional project detail (task 1002334, goal 1000046) ────────────── PATCH /provisioning/instances/:id/detail — answer (or re-answe… |
504
+ | POST | `/api/bongos/provisioning/instances/:id/disconnect` | `any-builder` | `confirm_slug` | POST /provisioning/instances/:id/disconnect — rank: any authenticated builder (own resource); an archon may disconnect any project. |
504
505
  | POST | `/api/bongos/provisioning/instances/:id/domain` | `any-builder` | `domain` | POST /provisioning/instances/:id/domain — give the caller's OWN instance its address (task 1002692, gap 5: no path existed to add a domai… |
505
506
  | POST | `/api/bongos/provisioning/instances/:id/force-teardown` | `metic+archon` | — | POST /provisioning/instances/:id/force-teardown — Archon tears down ANY instance. |
506
507
  | POST | `/api/bongos/provisioning/instances/:id/github-app` | `any-builder` | `account` | POST /provisioning/instances/:id/github-app — mint a GitHub-App-Manifest onboarding link for the caller's OWN instance (task 2080, ADR 01… |
@@ -593,7 +594,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
593
594
  | GET | `/api/bongos/sso/authorize` | `public` | — | GET /sso/authorize?client_id=&redirect_uri=&state= — the browser authorize leg. |
594
595
  | POST | `/api/bongos/sso/device/poll` | `public` | `device_code` | POST /sso/device/poll — poll the hub device flow. |
595
596
  | POST | `/api/bongos/sso/device/start` | `public` | `origin` | POST /sso/device/start — begin CLI federation. |
596
- | POST | `/api/bongos/sso/membership/check-in` | `public` | `client_id`, `client_secret`, `github_id`, `github_login`, `display_name`, `avatar_url`, `membership_kind`, `role` | POST /sso/membership/check-in — a federated instance reports, on every sign-in, that a builder builds on its project (ADR 0141 §3). |
597
+ | POST | `/api/bongos/sso/membership/check-in` | `public` | `client_id`, `client_secret`, `github_id`, `github_login`, `display_name`, `avatar_url`, `membership_kind` | POST /sso/membership/check-in — a federated instance reports, on every sign-in, that a builder builds on its project (ADR 0141 §3). |
597
598
  | GET | `/api/bongos/sso/pubkey` | `public` | — | GET /sso/pubkey — the hub's Ed25519 assertion-verification key (public). |
598
599
  | POST | `/api/bongos/sso/token` | `public` | `client_id`, `client_secret`, `code`, `redirect_uri` | POST /sso/token — server-to-server code redemption. |
599
600