@bongos/core 1.20.23 → 1.20.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.20.23",
6
- "core_contract": "1.20.23",
7
- "source_commit": "53e1513a015b5e684c3706c493675c2fd9f890b0",
5
+ "core_version": "1.20.25",
6
+ "core_contract": "1.20.25",
7
+ "source_commit": "c77a95cf8a90470fbccd7a49fe8cadeac1c3d623",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-30T16:54:47.028Z",
9
+ "built_at": "2026-09-30T18:59:50.459Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 558,
12
+ "docs_redacted": 560,
13
13
  "agent_docs_stubbed": 25,
14
- "functional_verbatim": 2643,
14
+ "functional_verbatim": 2645,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3227,
20
- "tree_sha256": "be76bb6bac10f8f8e03cbc19031c0a0e2866729e3ee9cfce9bf0b9a0704a0c5d",
19
+ "file_count": 3231,
20
+ "tree_sha256": "be259f84dcf01e7c06053ca8c31f196c8f3084f86b1579f55ff9f6aac33e30ad",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2239,10 +2239,15 @@
2239
2239
  "mode": "0000644",
2240
2240
  "sha256": "4445d9c306a22fb3b7e72467bedad262f4653c092bf4866f8a39410248da2724"
2241
2241
  },
2242
+ {
2243
+ "path": "docs/adr/0354-a-builder-hosts-one-project-free-and-more-takes-the-fleet-permission.md",
2244
+ "mode": "0000644",
2245
+ "sha256": "962d868c0887244c53b53c58754f7232715a20df6feec694f30cbda707f9e17e"
2246
+ },
2242
2247
  {
2243
2248
  "path": "docs/adr/README.md",
2244
2249
  "mode": "0000644",
2245
- "sha256": "6be10fb1925fd406729e08b4d604569b1123b7c5f6091300559df8532831ab69"
2250
+ "sha256": "850168519d3ef7eaf6d9658b9e2b19a80402147e9f3e8fc5318c53b7647fa9b0"
2246
2251
  },
2247
2252
  {
2248
2253
  "path": "docs/api-reference.md",
@@ -2252,7 +2257,7 @@
2252
2257
  {
2253
2258
  "path": "docs/api/openapi.json",
2254
2259
  "mode": "0000644",
2255
- "sha256": "f44cf9111baee003af84ab6c83503e97a4a723c4cb2ad59ad367cce75f8f717e"
2260
+ "sha256": "787e1af5323f03257c136d5ef9e15eb58e2cdc6abf8b90ddbc94b479ba1659a4"
2256
2261
  },
2257
2262
  {
2258
2263
  "path": "docs/architecture.md",
@@ -2354,6 +2359,11 @@
2354
2359
  "mode": "0000644",
2355
2360
  "sha256": "dfa1ea2c45eddd5a9528487f3a0fe80398ba9018c5f73706629015ff6c2108e0"
2356
2361
  },
2362
+ {
2363
+ "path": "docs/design/project-startup-direction.md",
2364
+ "mode": "0000644",
2365
+ "sha256": "b7c67956a0fe3c38efc22701741b4e19ba383c942b864d2b37b195619594392f"
2366
+ },
2357
2367
  {
2358
2368
  "path": "docs/design/projects-hub-direction-v2.md",
2359
2369
  "mode": "0000644",
@@ -2782,7 +2792,7 @@
2782
2792
  {
2783
2793
  "path": "docs/module-api-changelog.md",
2784
2794
  "mode": "0000644",
2785
- "sha256": "e4970a8a4af13ed70d73aafe8ec6b5c96999a8f23764dd32da86f96d538dfdc6"
2795
+ "sha256": "76eb8564b3b574c106fe2d1616ac7830a4a30a3d0237bf3f4bad78c0d0ecd43c"
2786
2796
  },
2787
2797
  {
2788
2798
  "path": "docs/modules-contract.md",
@@ -2872,7 +2882,7 @@
2872
2882
  {
2873
2883
  "path": "docs/page-readings.json",
2874
2884
  "mode": "0000644",
2875
- "sha256": "dd7227bb9daface49cfb1e2a8702cb81b95a5d19f0169038ddfd7a6c80e04225"
2885
+ "sha256": "3b5310724bcecee35da76de11b3d6c1a9f3fe404b7c79713ee6d97f3dc09d739"
2876
2886
  },
2877
2887
  {
2878
2888
  "path": "docs/project-context.template.md",
@@ -7269,6 +7279,11 @@
7269
7279
  "mode": "0000644",
7270
7280
  "sha256": "f00a21da3c27933cd34cb66a7d99ef1104eac1180d0655e8fa59e96f9a366ace"
7271
7281
  },
7282
+ {
7283
+ "path": "modules/provisioning/free-place-lock.js",
7284
+ "mode": "0000644",
7285
+ "sha256": "82b9f7ee39820735ca4d0f55b12fd72903b9909d3c9f0abedb684dcc98ac77c6"
7286
+ },
7272
7287
  {
7273
7288
  "path": "modules/provisioning/migrations/provisioning_001_tables.sql",
7274
7289
  "mode": "0000644",
@@ -7422,7 +7437,7 @@
7422
7437
  {
7423
7438
  "path": "modules/provisioning/paid-shape-gate.js",
7424
7439
  "mode": "0000644",
7425
- "sha256": "7f7f9b1a6ffcf2f58d3a3a7b5d5244a3474f0245274a7bd6777befba36c5fc9c"
7440
+ "sha256": "aad8d8e8a641ebcbff9aeeed765e6c1657fa5896dc9cc3e84aa32f6d327b7db2"
7426
7441
  },
7427
7442
  {
7428
7443
  "path": "modules/provisioning/pollers/app-liveness.js",
@@ -7512,7 +7527,7 @@
7512
7527
  {
7513
7528
  "path": "modules/provisioning/routes/provisioning.js",
7514
7529
  "mode": "0000644",
7515
- "sha256": "18a8a728c48070f99dfdaa1c21d738f8d6fcfe8dc8534574fc115cec77f8bf77"
7530
+ "sha256": "d0a1e16b8ba6cb03bfadf95c7a3b688007309d173812e34b4e69d6e4dc33d21f"
7516
7531
  },
7517
7532
  {
7518
7533
  "path": "modules/provisioning/routes/render-standup.js",
@@ -8067,7 +8082,7 @@
8067
8082
  {
8068
8083
  "path": "modules/public-landing/public/projects.html",
8069
8084
  "mode": "0000644",
8070
- "sha256": "b2780277742b8f560dc431ce77498ffdefaf1a027ffde893caa472e59da512f3"
8085
+ "sha256": "dc489131cceef3330ea6d3ee0d24c05a723076478496efb46ce8e509381aea03"
8071
8086
  },
8072
8087
  {
8073
8088
  "path": "modules/public-landing/public/projects.probes.json",
@@ -8912,12 +8927,12 @@
8912
8927
  {
8913
8928
  "path": "package-lock.json",
8914
8929
  "mode": "0000644",
8915
- "sha256": "417e99397bbe709aa8cd87385188a162f83ee135c12954f2cf92a23a499f7b3f"
8930
+ "sha256": "d966dd1820b8e6c02522cc7b3e839e2b6d306b782417364cefd51641801f74b3"
8916
8931
  },
8917
8932
  {
8918
8933
  "path": "package.json",
8919
8934
  "mode": "0000644",
8920
- "sha256": "2cdc66718b40d75fb0046f4b7dc17ae36ac4c18451ab90e86d5a32b93e34bb4c"
8935
+ "sha256": "cbed81083b8dfa0c558b28d5567ff392c575544cfe562b316bc1ed4d533b0bd4"
8921
8936
  },
8922
8937
  {
8923
8938
  "path": "public-docs/index.html",
@@ -8937,7 +8952,7 @@
8937
8952
  {
8938
8953
  "path": "release-notes.json",
8939
8954
  "mode": "0000644",
8940
- "sha256": "a37f6b56d6237e71dbb1f2d70cb703acd869f8a0ac5ca8e1d27550b3f8fe1bc1"
8955
+ "sha256": "306aa139c1c83d1276d29c20d9e7c7e26e3073f0175c610f89c27e6a2428ccee"
8941
8956
  },
8942
8957
  {
8943
8958
  "path": "scripts/bongos-mcp.js",
@@ -11052,7 +11067,7 @@
11052
11067
  {
11053
11068
  "path": "src/module-api.js",
11054
11069
  "mode": "0000644",
11055
- "sha256": "a1d0a3dd719c8550dea1190993a548ea01cad01414e143b32055f18e2cb4e10d"
11070
+ "sha256": "e200ecfa45721d0febc5e7700c7cd212a1d8c486b8c861b5ea6d228b5961275e"
11056
11071
  },
11057
11072
  {
11058
11073
  "path": "src/module-loader/catalog.js",
@@ -14467,7 +14482,7 @@
14467
14482
  {
14468
14483
  "path": "tests/project_catalog_routes.mjs",
14469
14484
  "mode": "0000644",
14470
- "sha256": "90a8283375de41b753c28096ef5c286146322857c10b83124c52748df9ba570d"
14485
+ "sha256": "e19d61fb2734092104276d1f74f1cbdf691bfe4f920414c543a1c07e839725e6"
14471
14486
  },
14472
14487
  {
14473
14488
  "path": "tests/project_door_ui.mjs",
@@ -14552,7 +14567,7 @@
14552
14567
  {
14553
14568
  "path": "tests/projects_hub_pre_uat.mjs",
14554
14569
  "mode": "0000644",
14555
- "sha256": "c498363abc196c09cfc30b4e69b4cc943dd293ebecb9aa7b4319c92721a82f30"
14570
+ "sha256": "5a2d7c53cf71d22dc7a7f4547c9dc59746002e35330a92c7047e4742c1977073"
14556
14571
  },
14557
14572
  {
14558
14573
  "path": "tests/projects_hub_render_connect.mjs",
@@ -14662,7 +14677,7 @@
14662
14677
  {
14663
14678
  "path": "tests/provisioning_capacity_gate.mjs",
14664
14679
  "mode": "0000644",
14665
- "sha256": "406ea4d32fe1bbb7aa1f69e0c4dc967feb2fc43c8bf0f70848547fdc6faf8762"
14680
+ "sha256": "478f4e06bd11f64e69efd9482094f262ac29016d052117c6a866cc9f0d55c318"
14666
14681
  },
14667
14682
  {
14668
14683
  "path": "tests/provisioning_cost_ledger.mjs",
@@ -14687,7 +14702,7 @@
14687
14702
  {
14688
14703
  "path": "tests/provisioning_domain_attach.mjs",
14689
14704
  "mode": "0000644",
14690
- "sha256": "cd0f81de7dd909fb1db27f86be4f541e38ec36f6d76cf97f7e7c759565f9d392"
14705
+ "sha256": "160acd275e3b466088ee5913186d430e974973a4e3eee51f3d62d8604a8fd64f"
14691
14706
  },
14692
14707
  {
14693
14708
  "path": "tests/provisioning_domain_lookup_index.mjs",
@@ -14727,7 +14742,7 @@
14727
14742
  {
14728
14743
  "path": "tests/provisioning_paid_shape_gate.mjs",
14729
14744
  "mode": "0000644",
14730
- "sha256": "f6f2a28ebadee97523019e3346f69f399864c6bf23b4e14029d6df81dee872cb"
14745
+ "sha256": "c9fa4aa59e6e47c139277e3c57298ca441180c4d4752ca6994c72678219df8c8"
14731
14746
  },
14732
14747
  {
14733
14748
  "path": "tests/provisioning_public_refusal.mjs",
@@ -14787,13 +14802,18 @@
14787
14802
  {
14788
14803
  "path": "tests/provisioning_teardown_intent.mjs",
14789
14804
  "mode": "0000644",
14790
- "sha256": "08128da3c6b810df963cb167cf29608181627d6c6203efe57f27d1c29e22d54a"
14805
+ "sha256": "4d06c5fd13f8cc81c050c2ed6940c91db5cdd95dea9f26ad7b8d6a8089c0254a"
14791
14806
  },
14792
14807
  {
14793
14808
  "path": "tests/provisioning_teardown_states.mjs",
14794
14809
  "mode": "0000644",
14795
14810
  "sha256": "1b5decd026e6487c9477b87f533852dd93821b6688c3c76e694041b3918ee420"
14796
14811
  },
14812
+ {
14813
+ "path": "tests/provisioning_wizard_create_xenos.mjs",
14814
+ "mode": "0000644",
14815
+ "sha256": "2b7048eb35f09fd40f93e301e7c7e999db1b12b04bd0855e616b8ae1d93006a8"
14816
+ },
14797
14817
  {
14798
14818
  "path": "tests/pruned_lockfile_integrity.mjs",
14799
14819
  "mode": "0000644",
@@ -0,0 +1,46 @@
1
+ # ADR 0354 — A builder hosts one project for free, and more takes the fleet permission
2
+
3
+ - **Status:** accepted
4
+ - **Date:** 2026-09-30
5
+ - **Task:** [task 1004412](https://cloudbongos.com/builders#/task/1004412) (goal 1000106 — working area 1, Project creation)
6
+ - **Amends:** the gate [task 1003370](https://cloudbongos.com/builders#/task/1003370) put on `cloud-host` (audit B4), for a builder's first hosted project only
7
+ - **Builds on:** [ADR 0345](0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md) decision 1 (we host every project's hall, and creating a project needs no platform account, key or bill) · [ADR 0285](0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md) (the shared box holds about fifteen projects)
8
+ - **Decided with:** the owner chose D1 on 2026-09-30, from three options put to them in plain language.
9
+
10
+ ## Context
11
+
12
+ The create wizard sends one request when the owner presses its last button, "Create my project", and that request always asks for `cloud-host`: the hall, hosted by us. Task 1003370 (2026-09-19) gated that shape behind `provisioning.fleet.manage` (floor: Metic), because a hosted hall runs code from its owner's repository on the platform's own box. A new hub signup is a Xenos. So from that day, every new user who pressed "Create my project" was refused with `permission_forbidden`. The wizard showed it as its generic "We couldn't file the request just now", with the permission name underneath. Task 1004412 confirmed this with a route-level test that uses the real permission gate and a Xenos's real grants.
13
+
14
+ ADR 0345 came nine days later and made `cloud-host` the free front door. The two decisions contradicted each other, and no test covered the path they shared.
15
+
16
+ ## Decision
17
+
18
+ ### D1 — One live hosted project per builder needs no permission
19
+
20
+ A builder may create a `cloud-host` project with no permission if they hold no other live `cloud-host` project. "Live" means any status except `torn_down`. A queued or failed project still holds its place, and its owner can retry it or take it offline. Asking again for your own project's slug (a retry or a revive) counts as that same project, not a second one. A further live hosted project needs `provisioning.fleet.manage`, and the refusal is the kernel gate's own `permission_forbidden`.
21
+
22
+ What stops one person taking every place on the box is this limit, not rank. `byo-host` stays open without limit, and `control-plane` and `co-tenant` stay unrequestable. The gate is still an allow-list, so a shape added later is gated by omission.
23
+
24
+ If the count cannot be read, the gate refuses with `request_failed`. It is an authority gate, so it fails closed (ADR 0016).
25
+
26
+ ### D2 — One first-free request per builder at a time
27
+
28
+ The count and the insert are two separate steps, so two requests sent together would both count zero. The gate takes a Postgres advisory lock keyed on the builder (`free-place-lock.js`, a try-lock on its own connection) before it counts, and lets it go as soon as the route has inserted the row (or when the response closes, on any earlier exit). A second request sent in the meantime, from any web process, gets 409 `create_in_progress`. A lock that cannot be taken refuses, like an unreadable count. (The first version used a process-local mark; the grader flagged that it would not hold across processes, so it moved into the database before shipping.)
29
+
30
+ ### D3 — The wizard states both refusals in plain words
31
+
32
+ - **`permission_forbidden` with a readable `held`:** "Each account can host one project for free, and yours already has one…". The sentence names the project page's own "Take it offline" control, and says that trusted builders can host more than one (the owner asked for that line when reviewing the wording).
33
+ - **`box_full`:** "Our shared server is full right now…", instead of the operator's sentence.
34
+ - **`permission_forbidden` with `held: null`:** authority could not be checked at all, so the wizard keeps its generic sentence.
35
+
36
+ ## Consequences
37
+
38
+ - New users can create a project from the wizard again. Each project costs us about $0.80 a month (ADR 0285).
39
+ - A stranger's code now runs on the shared box, on the same server as the hub. It runs in its own locked account (task 1003369), and the hub's sign-in cookie no longer reaches project halls (task 1004357). **Nothing caps a hosted project's memory or CPU**, so one misbehaving project can slow every hall and the hub. That was already true of every hosted project; this decision widens who can have one.
40
+ - Each first-free request holds one database connection from its count to its insert. Creates are rare, so the pool can carry it.
41
+
42
+ ## Alternatives considered
43
+
44
+ - **Open to everyone, limited only by the box's capacity.** The smallest change. But one person could take every free place, and the capacity gate would then refuse everyone else.
45
+ - **Trusted builders only, with a plain refusal.** Keeps task 1003370's rule whole. But new users still could not start a project, which ADR 0345 decision 1 rules out.
46
+ - **An approval queue for new users' projects.** Keeps the rule and admits new users. But it is new machinery, and it would collide with the owner's redesign of the creation flow (begun 2026-09-30).
@@ -472,3 +472,4 @@ These 20 numbers are each shared by exactly two files. They are **accepted histo
472
472
  | 0351 | [**A criterion closes on a UAT, not on its linked tasks shipping** ([task 1004392](https://cloudbongos.com/builders#/task/1004392), goal 1000089 — working area 3). Supersedes ADR 0183's close rule. `wa7-government` had closed on six Board Room navigation tasks while nothing it describes was on the live site. **D1:** three checks — Code (linked tasks done, ≥1 shipped; automatic), Live, UAT — and the sweep closes a criterion only with a CURRENT sign-off (no older than the newest linked ship). **D2 (owner):** "Awaiting UAT" is a visible state. **D3 (owner):** one step; the signer is refused if they shipped linked work, the project owner excepted. **D4 (owner):** an mp4/webm up to 100 MB on the project's own server. **D5 (owner):** backend-only is set while a criterion is open and takes a recording-free sign-off. **D6 (owner):** Live is read via the `deploy.taskWhere` port where the site can tell, attested otherwise. **D7 (owner):** already-closed criteria stay closed ("Closed before UAT"). **D8:** `/satisfy` is a recorded override that needs a reason. **D9:** `/goal-uat` + `scripts/gds/uat.js`; the hall is task 1004402.](0351-a-criterion-closes-on-a-uat.md) | lifecycle / criteria / review |
473
473
  | 0352 | [**A rollup report carries its "as of" in a header, and the hub keeps the newest** ([task 1004268](https://cloudbongos.com/builders#/task/1004268), goal 1000110). `POST /sso/activity/rollup` was last-writer-wins with no report timestamp, so a sign-in push read earlier and a ship push read later could land the older totals last. **D1:** the instance's read time rides a `Bongos-Report-As-Of` HEADER, not a body field, because the hub validates the body strictly and hub and instances upgrade independently: a body field from a newer instance would 400 on an older hub and lose the report, while an unknown header is ignored. **D2:** missing or unparseable = unstamped = last-writer-wins, never a 400. **D3:** the guard is a WHERE on the upsert's conflict branch (migration 026, `reported_as_of`, no backfill); a skipped update keeps the row lock, so the task 1004244 snapshot stays in step. **D4:** the stamp is compared only with the same client's, and clamped to the hub's `now()`.](0352-a-rollup-report-carries-its-as-of-in-a-header.md) | platform identity / federation |
474
474
  | 0353 | [**A hub invite is a notice of the project's own invite, and signing in accepts it** ([task 1002818](https://cloudbongos.com/builders#/task/1002818), goal 1000110 — R6 of the admin console wave). The hub's own invite wrote a `pending` membership that no project reads, so an "accepted" invite met a locked door (new projects default to `apply`). **D1:** that row is now a NOTICE the PROJECT writes: its invite route calls `POST /sso/invites/notify` and a dismissal that leaves no invited row calls `/sso/invites/clear` (client credentials, pending rows only, always `{ ok: true }` so no account oracle); core's `notifyHubOfInvite` is fire-and-forget. **D2:** the hub-side accept route is deleted; signing in to the project accepts, and the hub's own sign-in witness turns the row into a membership. **D3 (owner):** `POST /projects/invite` writes nothing and answers `{ invited: false, invite_url }`, the project's hall invite link (`/builders/watch?invite=<login>`, which only pre-fills); the signed-assertion relay is deferred. **D4:** new routes, so an older hub 404s and only the notice is lost (ADR 0352).](0353-a-hub-invite-is-a-notice-of-the-projects-own-invite.md) | platform identity / federation |
475
+ | 0354 | [**A builder hosts one project for free, and more takes the fleet permission** ([task 1004412](https://cloudbongos.com/builders#/task/1004412), goal 1000106 — working area 1, the owner's ruling of 2026-09-30). The create wizard always asks for `cloud-host`, which task 1003370 had gated behind `provisioning.fleet.manage`, so every new (Xenos) user was refused at "Create my project" — against ADR 0345 decision 1. **D1:** a builder's first live `cloud-host` project needs no permission; a further one needs the fleet permission (a retry of your own slug is the same project; only `torn_down` frees the place; an unreadable count refuses). **D2:** one first-free request per builder at a time (409 `create_in_progress`), held as a Postgres advisory lock so it holds across processes. **D3:** the wizard states `permission_forbidden` and `box_full` in plain words. Nothing yet caps a hosted project's memory.](0354-a-builder-hosts-one-project-free-and-more-takes-the-fleet-permission.md) | provisioning / security / project creation |
@@ -14418,7 +14418,7 @@
14418
14418
  "provisioning"
14419
14419
  ],
14420
14420
  "summary": "POST /provisioning/instances",
14421
- "description": "POST /provisioning/instances — request a new instance. rank: any authenticated builder (own resource) for the free `co-tenant` shape. Every other shape also needs provisioning.fleet.manage — `dedicated` because it bills, `standalone` because it runs the caller's own repo on the platform's box — and `control-plane` is refused outright, since the platform enrolls its own row (../paid-shape-gate.js, task 1003370 / audit B4). 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 free; others gated), 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).",
14421
+ "description": "POST /provisioning/instances — request a new instance. rank: any authenticated builder (own resource) for `byo-host`, and for their FIRST live `cloud-host` (task 1004412, ADR 0354). A further cloud-host needs provisioning.fleet.manage, and `control-plane` / `co-tenant` are refused outright (../paid-shape-gate.js). 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? (byo-host free; one cloud-host free), 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).",
14422
14422
  "x-rank": "any-builder",
14423
14423
  "x-source": "modules/provisioning/routes/provisioning.js",
14424
14424
  "requestBody": {
@@ -0,0 +1,131 @@
1
+ # Project startup — design direction (the design of record)
2
+
3
+ > Goal 1000121 · task BV2.PS01 (1004415) · spec [`docs/specs/<redacted>.md`](../specs/<redacted>.md) (final) · idea 1001172.
4
+ > Live prototype: the Claude Design canvas "Project Startup Onboarding" (https://claude.ai/artifact/<redacted>). It is private to the owner. Its sources are committed in [`mocks/project-startup/`](mocks/project-startup/README.md).
5
+
6
+ This document is what every build task in goal 1000121 cites. Name a screen by its canvas file (for example "`Charter.dc.html`") and look it up here. It records:
7
+
8
+ - **what each screen is**;
9
+ - **what the live product KEEPS**;
10
+ - **what the new design ADDS**;
11
+ - the decisions that govern both.
12
+
13
+ ## The blend rule — read this first (owner, 2026-09-30)
14
+
15
+ > *No UI battle between existing and new. Build the prototype's design, then blend it where needed with existing design. Don't hijack the builders hall.*
16
+
17
+ Four consequences, applied in every row below:
18
+
19
+ 1. **Genesis is a MODE the existing hall wears, never a second hall.**
20
+ - It follows the social-mode precedent (`html[data-social]`): one attribute on the hall, off by default, and off whenever the founding state cannot be read.
21
+ - It keeps today's v3 glass hall: the shell, the 64px rail, the navigation groups, the panels, the tokens and both modes.
22
+ 2. **Founding mode adds exactly four things, and nothing else changes:**
23
+ - the founding band;
24
+ - the genesis home;
25
+ - the asleep state on existing navigation items;
26
+ - the founding mark.
27
+
28
+ A test must pin that a hall that is not founding renders exactly as today.
29
+ 3. **Where the canvas's hall chrome differs from the live hall, the live hall wins.**
30
+ - The canvas drew a hall to show the idea: an expanded 232px rail, its own panel spacing, its own icons. Those are illustration, not instruction.
31
+ - Build the four additions *into* the real hall's components (`OTBKit`, the archetype sheets, `shell.js` NAV, the widget registry). Never port the canvas's markup.
32
+ 4. **The hub-side screens (front door, questions, look) are new surfaces in the apex world.**
33
+ - They extend the existing create wizard (`projects.html ?view=new`) and follow `DESIGN.md` exactly.
34
+ - Where the wizard already has a component, reuse it rather than the canvas's version: the lifecycle rail, hairline choice cells, one-hairline fields, doors and ledger rows.
35
+
36
+ ## The screens
37
+
38
+ ### Front door (hub, apex world)
39
+
40
+ | Canvas screen | What it is | Keep | Add |
41
+ |---|---|---|---|
42
+ | `Main.dc.html` · Before you begin | A terms acknowledgement before creation. | Sign-in stays where it is today, before this screen. The terms version stamp (the platform's existing acceptance machinery) records what was accepted. | A new first screen: display headline, two lede paragraphs, one checkbox, a "read the terms" link, one primary door. |
43
+ | `Fork.dc.html` · Demo or create | "You are now creating a digital planet." | The world's **door pill**, as specified in `DESIGN.md`: two equal doors joined into one glass object. | A full-bleed composition: the seed on the field plate, the headline, the door pill, and two route descriptions on a hairline band. |
44
+
45
+ ### Route 1 · the demo
46
+
47
+ | Canvas screen | What it is | Keep | Add |
48
+ |---|---|---|---|
49
+ | `DemoSetup.dc.html` | People, hours each, an idea, and a fit check. | The wizard's steppers-as-fields grammar and hairline fields. | A fit-check panel in Bongos's voice, and a time-box ring on the planet. The copy says each person uses their own AI plan. |
50
+ | `DemoReady.dc.html` | Defaults picked, plus invites. | Ledger rows for the defaults; the existing invite step's shape. | The demo defaults (D-list below) and two invite fields. |
51
+ | `DemoKeep.dc.html` | "Keep your planet?" | — | Two cells: carry it over (pre-fills part 1) or keep it as a demo. |
52
+
53
+ **Demo decisions:**
54
+ - **D3 — the time box is soft.** A countdown shows; at zero the demo asks "keep your planet?". Work is never frozen.
55
+ - **D9 — an unkept demo is archived after 30 days.** It goes offline and can be restored, and the owner is warned first.
56
+
57
+ ### Route 2 · part 1, planet physics (hub, apex world)
58
+
59
+ Every question screen shares one layout. On the left are the rail of seven steps, the question, the choices and the doors. On the right is the **stage**: the field plate, with the planet forming.
60
+
61
+ | Canvas screen | Question | Keep | Add |
62
+ |---|---|---|---|
63
+ | `Autofill.dc.html` | Start from your account, or from scratch? | Hairline cells. | The seven-step rail, shown up front. |
64
+ | `State.dc.html` / `StateExisting.dc.html` | 1 · New or existing, the idea text, and existing work types. | Cells, a hairline textarea, and check rows. | The planet begins as a **seed** (new) or **already formed** (existing). |
65
+ | `NotPossible.dc.html` | The screening stop. | — | A calm stop screen: nothing created, contact us, start over. No planet forms. |
66
+ | `Geography.dc.html` | 2 · Home country and state, and where builders are. | Hairline selects and option rows. | The planet's **coordinates** (a mono line on the stage). |
67
+ | `Ages.dc.html` | 3 · Anyone under 18? | Cells, and a ledger for the tiers. | A **protective ring**; the guardian tiers stated plainly. |
68
+ | `Entity.dc.html` / `EntityExisting.dc.html` | 4 · Intent (new) or legal type (existing). | Option rows with group heads. | The planet's **core material**: frost for an individual, chrome for a group, gold for an organization, obsidian for a nonprofit. This is placeholder art direction. |
69
+ | `Builders.dc.html` / `BuildersPro.dc.html` | 5 · One or many, how many, and a growth guess. | Cells and a stepper. | **Moons**, plus a dashed orbit for expected growth. Two voices (D5). |
70
+ | `Rewards.dc.html` | 6 · Reward intents (hidden when solo). | Check rows. | **Rings**: currents of value. |
71
+ | `Framing.dc.html` | 7 · A–D, plus the service checklist. | Option rows and a two-column check grid. | The planet's **orbit** around the Bongos pair: all managed is close, hybrid is middle, local is far, open source is its own system. |
72
+ | `Physics.dc.html` | The summary. | Ledger rows with change links. | The full planet, with every addition. |
73
+
74
+ **Decisions for part 1:**
75
+ - **D5 — the voice follows the entity answer automatically.** Planet voice for individuals and groups; professional voice for organizations. It can be switched later in settings.
76
+ - **D6 — soft limits only.** An option that does not apply is hidden or greyed, with a plain "why" line from one rule table that carries its reasons. Nothing claims to be enforced.
77
+
78
+ ### Coordinates, a look, then birth (hub, apex world)
79
+
80
+ | Canvas screen | What it is | Keep | Add |
81
+ |---|---|---|---|
82
+ | `Coordinates.dc.html` | Name, handle and address; where the code lives; where the app runs. | **Today's wizard steps as they are.** This screen only shows them **pre-filled from part 1**, and skipped when they do not apply. | The planet named under the stage. |
83
+ | `Look.dc.html` | Pick a look for light and dark mode, plus a logo. | **A look is a branding pack**: the style library's real looks (Chrome World, Expedition, Grove, Blueprint), with no sixteenth token. | A look picker with two mini-hall previews. A night-tint switch, **on by default** (D2 — its ADR is PS06's first job). A logo upload. |
84
+ | `Birth.dc.html` | The planet forming while setup runs. | **Today's done panel**: the standup rail and the three owner-only steps. | The planet arriving on the stage. The primary door starts genesis while the machine setup finishes. |
85
+
86
+ **D8 — existing projects get the look and the logo only**, from project settings. They never get genesis.
87
+
88
+ ### Part 2 · genesis, planet biology (the builders' hall, founding mode)
89
+
90
+ **Every row here is governed by the blend rule.** The canvas shows Kestrel's chosen look (Grove, with the night tint) on purpose, to show that halls differ. The real hall renders the project's own branding pack, whatever it is.
91
+
92
+ | Canvas screen | What it is | Keep (the live hall) | Add (founding mode only) |
93
+ |---|---|---|---|
94
+ | All four genesis screens | The frame. | The v3 glass hall: rail, topbar, panels, tokens, both modes, and the project's logo in the topbar. | **The founding band**: a 44px strip above the topbar reading "project founding mode", with the stage and day and a "what's this?" link. **Asleep navigation items**: today's items, dimmed, each naming the stage that wakes it. |
95
+ | `GenesisDecide.dc.html` | Decide: stage lengths, what counts as done, the starting government, passes, advisors. | The hall's panels, ledger rows and pills. | **The genesis home** as the founding-mode landing page, including a solo fast-track (D4, about ten minutes). |
96
+ | `GenesisHall.dc.html` | Mid-genesis. | The home widget registry and the roster rows. | A **life strip** (one rendered plate per stage; PS12 renders them), the stage list, and a founding builders panel. |
97
+ | `Charter.dc.html` | Pick a form of government. | The charter library's real forms and trajectories. | Forms under their **real academic names** with plain explanations (D10). The founding builders' reviews are shown. |
98
+ | `Legislation.dc.html` | Fine-tune the variables. | The Board Room constitution: franchise plus pass rule. | The variables under their academic names with plain explanations. The unbuilt variables are shown as coming later. "Limits from your answers" (D6). |
99
+ | `Alive.dc.html` | The planet comes alive. | The hub sky: the planet joins it. | A moment screen: the founding builders credited, then enter the full hall. **Founding mode switches off.** |
100
+ | `FoundingBuilders.dc.html` | The founding mark, after genesis. | The ranks page, profile and roster exactly as they are. | **The founding mark**: a numbered seed badge (D1 — badge only, it grants nothing), a column on ranks, and a founding wall on the home page. |
101
+
102
+ **How rooms wake.** Each finished stage wakes today's navigation items:
103
+
104
+ | Stage finished | Wakes |
105
+ |---|---|
106
+ | scope | goals & roadmap |
107
+ | strategize | tasks |
108
+ | charter | board room |
109
+ | legalize | economy |
110
+ | legislation | ranks |
111
+ | alive | community, and the public sky |
112
+
113
+ **D7 — a stage closes when the constitution in force decides it** (a Board Room item; PS04). Under a monarchy that is the founder's single confirmation.
114
+
115
+ ## The planet as a system
116
+
117
+ The planet is built from the world's own rendered plates: `r2-*` and `r3-*`, the cutouts, and `r3-space-nebula` for the field. Nothing on the stage is drawn:
118
+
119
+ - orbits are hairline circles;
120
+ - moons are beads;
121
+ - the Bongos pair is the platform's mark.
122
+
123
+ Motion follows `DESIGN.md`: breathe (32s), sheen (40s), drift (90s), orbits of 160s or more, arrive (700ms). All of it is CSS on the objects only, and dead under the kill switch.
124
+
125
+ **The planet the flow forms is the planet that appears in the sky.** Its shape, material and style come from the hub's existing template fields, and the look's accent tints it.
126
+
127
+ ## Copy
128
+
129
+ - **The screen text is user-facing copy, drawn from the owner's scope doc.** Italic text and footnotes in that doc were internal; they live in the canvas's notes and in the spec.
130
+ - **Voice:** the planet voice keeps the owner's energy (one ":)" on the fork). The professional voice drops the planet words but keeps the same flow.
131
+ - **The sample data is sample.** Kestrel, Avery, Jordan, Sam and Rae, the counts, and the "[n]" placeholders are not product copy.
@@ -2703,5 +2703,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2703
2703
  landed since 1.20.21 with no explicit bump. run 36743446010. (task 1002620)
2704
2704
  1.20.23 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2705
2705
  landed since 1.20.22 with no explicit bump. run 36747567551. (task 1002620)
2706
+ 1.20.24 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2707
+ landed since 1.20.23 with no explicit bump. run 36759918201. (task 1002620)
2708
+ 1.20.25 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2709
+ landed since 1.20.24 with no explicit bump. run 36762454104. (task 1002620)
2706
2710
  ---------------------------------------------------------------------------
2707
2711
  ```
@@ -3606,7 +3606,7 @@
3606
3606
  "surface": "landing",
3607
3607
  "title": "{{productName}} — projects",
3608
3608
  "states_read": ["out-map","in-map","in-map-projects","in-map-profile","in-map-kbd","in-map-search","in-map-mine-on","in-table","in-table-by-builders","in-mine","in-manage","in-manage-address","in-manage-planet","in-manage-card","in-manage-kind","in-manage-invite","in-manage-invite-handoff","in-manage-app-live","in-manage-app-building","in-manage-app-failed","in-manage-app-retry","in-manage-app-offer","in-manage-app-connect","in-manage-snag","in-manage-gone","in-manage-missing","out-manage","in-new","out-new","in-new-type","in-new-about","in-new-modules","in-new-modules-search","in-new-home","in-new-address","in-new-app","in-new-app-render","in-new-review","in-new-done","in-new-resume-offer","out-map-projects","card","card-noart","card-self","in-mine-empty","card-floor","card-backdrop"],
3609
- "files_hash": "4d039ad9637045e2",
3609
+ "files_hash": "0175af500182c5d2",
3610
3610
  "reading_hash": "1b2c6ce40c79925e",
3611
3611
  "counts": {"lines":517,"placed":182,"shared":20,"unplaced":315},
3612
3612
  "omitted": {"data":107},
@@ -0,0 +1,52 @@
1
+ // modules/provisioning/free-place-lock.js — one first-free hosted-project request per
2
+ // builder at a time, across every web process (task 1004412, ADR 0354 D2).
3
+ //
4
+ // The shape gate counts a builder's live hosted projects and the route inserts the new
5
+ // one a few awaits later. Two requests sent together would both count zero and both take
6
+ // the free place. A process-local mark only closes that inside one process, so the guard
7
+ // is a Postgres advisory lock keyed on the builder: whichever request holds it answers,
8
+ // and any other request from that builder, from any process, is refused until it is let
9
+ // go. It is taken with TRY, so nothing ever queues behind it.
10
+ //
11
+ // Session-level, on a client held from the gate's count to the route's insert, because the
12
+ // count and the insert are not one transaction. Releasing unlocks and returns the client; if the
13
+ // unlock itself fails the client is destroyed, which ends the session and frees the lock.
14
+ //
15
+ // Its own file so a test can re-point `holdFreePlace` through the module object, and
16
+ // because provisioning.js and routes/provisioning.js sit at their size ratchets.
17
+ 'use strict';
18
+
19
+ // The lock's first key: one namespace for this guard, so it can never collide with an
20
+ // advisory lock another part of the platform takes on a builder id.
21
+ const NAMESPACE_SQL = "hashtext('provisioning.one-free-hosted-project')";
22
+
23
+ // holdFreePlace — try to hold the builder's free-place lock. Resolves to a release
24
+ // function when it is held, or null when another request already holds it. Throws when
25
+ // the database cannot be asked; the caller refuses then (it never admits on a guess).
26
+ async function holdFreePlace(db, builderId) {
27
+ const key = Number(builderId);
28
+ if (!Number.isInteger(key)) throw new TypeError(`free-place lock: builder id ${builderId} is not an integer`);
29
+ const client = await db.connect();
30
+ let held;
31
+ try {
32
+ const { rows } = await client.query(`SELECT pg_try_advisory_lock(${NAMESPACE_SQL}, $1::int) AS held`, [key]);
33
+ held = !!(rows[0] && rows[0].held);
34
+ } catch (err) {
35
+ client.release(true);
36
+ throw err;
37
+ }
38
+ if (!held) { client.release(); return null; }
39
+ let released = false;
40
+ return async function release() {
41
+ if (released) return;
42
+ released = true;
43
+ try {
44
+ await client.query(`SELECT pg_advisory_unlock(${NAMESPACE_SQL}, $1::int)`, [key]);
45
+ client.release();
46
+ } catch {
47
+ client.release(true);
48
+ }
49
+ };
50
+ }
51
+
52
+ module.exports = { holdFreePlace };