@bongos/core 1.19.395 → 1.19.396

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.19.395",
6
- "core_contract": "1.19.395",
7
- "source_commit": "c6682951454183e1d8561983b2bb2295aced7775",
5
+ "core_version": "1.19.396",
6
+ "core_contract": "1.19.396",
7
+ "source_commit": "b4632d6e8d92e8096e958762a36e810fc24c9c35",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-28T16:02:31.625Z",
9
+ "built_at": "2026-08-28T16:08:46.136Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 345,
13
13
  "agent_docs_stubbed": 23,
14
- "functional_verbatim": 1634,
14
+ "functional_verbatim": 1635,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2002,
20
- "tree_sha256": "354b4dc6de55dc65198160070a23c27d7aa950cd96962014f2f436a5ccfaefc8",
19
+ "file_count": 2003,
20
+ "tree_sha256": "950c8c43ac98c0e161e25a1c17cd699ad9582392105dcd0abadc8efc090a95c8",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -372,7 +372,7 @@
372
372
  {
373
373
  "path": "clients/bongos-client/index.d.ts",
374
374
  "mode": "0000644",
375
- "sha256": "e2924c48715177477a8f828e2118f832acad6b3f63ae7b4fe5f124e80a4e7c17"
375
+ "sha256": "88bfa3b136c58ce104a56cb2b7c922f11b55ae9e8ca0ec0d30d67bdb78ed70cf"
376
376
  },
377
377
  {
378
378
  "path": "clients/bongos-client/index.mjs",
@@ -1497,12 +1497,12 @@
1497
1497
  {
1498
1498
  "path": "docs/api-reference.md",
1499
1499
  "mode": "0000644",
1500
- "sha256": "3e488311a87f4db6ffbb85b26c7a61fba0204ff6cc1cc8ab6c09b2ae21cea24b"
1500
+ "sha256": "567afe22acead45477a501c61b9aea2f6a99eac1cfa3cf87e143f7b5077567ca"
1501
1501
  },
1502
1502
  {
1503
1503
  "path": "docs/api/openapi.json",
1504
1504
  "mode": "0000644",
1505
- "sha256": "6e93e2f283103158bc46062459b79b5528424ffb57f4730eb29a1cdba1b4976b"
1505
+ "sha256": "e53b70240119fe856005a7627177d26bf559ab60fb003db5a7a31274fa432616"
1506
1506
  },
1507
1507
  {
1508
1508
  "path": "docs/architecture.md",
@@ -1527,12 +1527,12 @@
1527
1527
  {
1528
1528
  "path": "docs/copy-inventory.md",
1529
1529
  "mode": "0000644",
1530
- "sha256": "fabaf96fa63e3bf2e690619ef11038b3503154368aeaa9c10d8eb2641e306dee"
1530
+ "sha256": "52655bf8c7a2c5860a1ce4486f1715769eba8b3e7162a4e43c78d582c935b7b0"
1531
1531
  },
1532
1532
  {
1533
1533
  "path": "docs/copy-registry.json",
1534
1534
  "mode": "0000644",
1535
- "sha256": "cfdea83dda157439808ea4d0ef9ba7af11e77ad4693b6dd135cdddc529754583"
1535
+ "sha256": "da37e58c70ef12e106d81be96072ee66ec52cb26ac4e624b18ca2455b90719d3"
1536
1536
  },
1537
1537
  {
1538
1538
  "path": "docs/design/apex-pass-2-direction.md",
@@ -3912,17 +3912,17 @@
3912
3912
  {
3913
3913
  "path": "modules/hall-ui/public/my-projects.css",
3914
3914
  "mode": "0000644",
3915
- "sha256": "5c702a9c2054287a8523b6901cd46e7caa1cb9580b7e8f04a3898b2c8bd82fd0"
3915
+ "sha256": "443e24fc3e403cfc97c36a8f81fe81a540be84cbe52b6af86a1476fb87bc44b6"
3916
3916
  },
3917
3917
  {
3918
3918
  "path": "modules/hall-ui/public/my-projects.html",
3919
3919
  "mode": "0000644",
3920
- "sha256": "49c63ef48e0fc6450ba15d76743c8220b5d5f27702843be71fa077551d92c35b"
3920
+ "sha256": "764221e2425c7430e83469fc865198e1020bcc8c5950aae6c3c1cecb49132721"
3921
3921
  },
3922
3922
  {
3923
3923
  "path": "modules/hall-ui/public/my-projects.js",
3924
3924
  "mode": "0000644",
3925
- "sha256": "25195cabf71890f93f065e2aa15e899d51c251807e59ba14a147150c5b6228a3"
3925
+ "sha256": "e664a08787727fcd7019fe3008e3b9d63d07b9ab8be5043a46d3fcac5218f261"
3926
3926
  },
3927
3927
  {
3928
3928
  "path": "modules/hall-ui/public/not-ready.html",
@@ -4737,7 +4737,7 @@
4737
4737
  {
4738
4738
  "path": "modules/platform-identity/routes/my-projects.js",
4739
4739
  "mode": "0000644",
4740
- "sha256": "4fadf91c0d40f3e5dd5d53f9ebe3ec825fd16a13aab41a76e8af25c911e11edf"
4740
+ "sha256": "fc948963cef6f7ef71b488cc5627310b6b217f3f62e13030b4da42a08e92796c"
4741
4741
  },
4742
4742
  {
4743
4743
  "path": "modules/platform-identity/routes/profile.js",
@@ -5212,12 +5212,12 @@
5212
5212
  {
5213
5213
  "path": "package-lock.json",
5214
5214
  "mode": "0000644",
5215
- "sha256": "58c9432c30c1c3c319aa7c526368a890cc9722773a0fb4f31bed0e59e1d5ebe9"
5215
+ "sha256": "7dd6552859e62daf201d9fdd1c17f95ea4a97bac3e5dc95d175ed0c8bbac88c7"
5216
5216
  },
5217
5217
  {
5218
5218
  "path": "package.json",
5219
5219
  "mode": "0000644",
5220
- "sha256": "8a4b801babd875eed0c1ca44c26640f0745a8e7a1fc82c51fea271333e68e62c"
5220
+ "sha256": "292bfaf5419d894c03339b6363173872ba73e896a9dceec60ff625568fe9ed60"
5221
5221
  },
5222
5222
  {
5223
5223
  "path": "public-docs/index.html",
@@ -7207,7 +7207,7 @@
7207
7207
  {
7208
7208
  "path": "src/module-api.js",
7209
7209
  "mode": "0000644",
7210
- "sha256": "5ead37acb3188ec29cea517b3b256b2882ca304152e30ecfea7d2ef82716bce2"
7210
+ "sha256": "711a3add8803459721523364c9c993e30908475bebdf65eb83789f5520e91104"
7211
7211
  },
7212
7212
  {
7213
7213
  "path": "src/module-loader/catalog.js",
@@ -8744,6 +8744,11 @@
8744
8744
  "mode": "0000644",
8745
8745
  "sha256": "d00e82184fbf7aab49c9b6efe5709b14f300ca0cea277a0eb140f073ded7ff50"
8746
8746
  },
8747
+ {
8748
+ "path": "tests/join_application_note.mjs",
8749
+ "mode": "0000644",
8750
+ "sha256": "6db72752d69ebdfcc82bdf4f0e2445d7c8a939f6b4b88c1915c9f1d2f8eb9d76"
8751
+ },
8747
8752
  {
8748
8753
  "path": "tests/join_relay_outcome.mjs",
8749
8754
  "mode": "0000644",
@@ -292,7 +292,7 @@ export interface PostModulesKeySubmitResponse { ok: boolean; submission: unknown
292
292
  export interface PostMyProjectsClientIdLeaveResponse { ok: boolean; left: unknown }
293
293
  export interface PostMyProjectsInvitesClientIdAcceptResponse { ok: boolean; accepted: boolean }
294
294
  export interface PostMyProjectsInvitesClientIdDeclineResponse { ok: boolean; accepted: boolean }
295
- export interface PostMyProjectsJoinRequest { origin: string }
295
+ export interface PostMyProjectsJoinRequest { origin: string; note?: string }
296
296
  export interface PostMyProjectsJoinResponse { ok: boolean; status: unknown; project: unknown; message: unknown }
297
297
  export interface PostOverrideRequestsIdDecideRequest { decision: string; note?: string }
298
298
  export interface PostProjectsInviteRequest { client_id: string; github_login: string }
@@ -18967,6 +18967,10 @@
18967
18967
  "origin": {
18968
18968
  "type": "string",
18969
18969
  "maxLength": 512
18970
+ },
18971
+ "note": {
18972
+ "type": "string",
18973
+ "maxLength": 400
18970
18974
  }
18971
18975
  },
18972
18976
  "required": [
@@ -395,7 +395,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
395
395
  | GET | `/api/bongos/my-projects/invites` | `any-builder` | — | the caller's OWN pending invitations (ADR 0141 §5). |
396
396
  | POST | `/api/bongos/my-projects/invites/:clientId/accept` | `any-builder` | — | accept an invite (pending → builder) for the caller's OWN account. |
397
397
  | POST | `/api/bongos/my-projects/invites/:clientId/decline` | `any-builder` | — | decline an invite (delete the pending row) for the caller's OWN account. |
398
- | POST | `/api/bongos/my-projects/join` | `any-builder` | `origin` | request to join a project. |
398
+ | POST | `/api/bongos/my-projects/join` | `any-builder` | `origin`, `note` | request to join a project. |
399
399
 
400
400
  ## `override-requests` (2)
401
401
 
@@ -4,13 +4,13 @@
4
4
  > Deterministic on a given tree — regenerate and commit rather than hand-editing.
5
5
  > Machine-readable twin: [`copy-registry.json`](copy-registry.json).
6
6
 
7
- **1533 strings** across 3 surfaces · 1334 distinct pieces of copy · 1533 certain, 0 heuristic · 154 shown in more than one place.
7
+ **1535 strings** across 3 surfaces · 1336 distinct pieces of copy · 1535 certain, 0 heuristic · 154 shown in more than one place.
8
8
 
9
9
  ## Surfaces
10
10
 
11
11
  | surface | lives at | files | strings |
12
12
  |---|---|---|---|
13
- | `builders-hall` | `modules/hall-ui/public` | 71 | 1128 |
13
+ | `builders-hall` | `modules/hall-ui/public` | 71 | 1130 |
14
14
  | `status-dashboard` | `modules/status-ui/public` | 4 | 98 |
15
15
  | `landing` | `public-landing` | 5 | 307 |
16
16
 
@@ -78,7 +78,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
78
78
  | `c10806b50c61` | 2x | Claim task {…} | modules/hall-ui/public/goals-render.js:280, modules/hall-ui/public/work.js:417 |
79
79
  | … | | _114 more in the JSON registry_ | |
80
80
 
81
- ## `builders-hall` — 1128 strings
81
+ ## `builders-hall` — 1130 strings
82
82
 
83
83
  | id | conf | text | where |
84
84
  |---|---|---|---|
@@ -135,6 +135,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
135
135
  | `6ca31dd5eb91` | certain | Acknowledge the primer | `modules/hall-ui/public/primer.html:65` |
136
136
  | `4461b13f95c2` | certain | Actions apply at once. Closing stops and deletes the box; you can start a fresh one anytime. | `modules/hall-ui/public/settings.html:162` |
137
137
  | `f6036360c90c` | certain | Activity, by craft | `modules/hall-ui/public/profile.html:84` |
138
+ | `b8476c677e7c` | certain | Add a note (optional) | `modules/hall-ui/public/my-projects.html:52` |
138
139
  | `62eded408097` | certain | Add {…} | `modules/hall-ui/public/profile-nudge.js:68` |
139
140
  | `7cf0163d4cde` | certain | Aggregate totals across every recorded work session. | `modules/hall-ui/public/sessions.html:35` |
140
141
  | `8b736e365191` | certain | All builders | `modules/hall-ui/public/hall-render.js:1039` |
@@ -148,6 +149,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
148
149
  | `69f0ca4a4d84` | certain | Analytics scope | `modules/hall-ui/public/hall-render.js:1037` |
149
150
  | `14bdceaac078` | certain | Answer the five questions and this spark becomes a Full Idea. | `modules/hall-ui/public/ideas.js:423` |
150
151
  | `52ed0e2e3470` | certain | Any rank | `modules/hall-ui/public/task-lib.js:62` |
152
+ | `1ab0e6d5b079` | certain | Anything the project's owner should know — what you'd like to work on, who you are. | `modules/hall-ui/public/my-projects.html:54` |
151
153
  | `c89833aa985a` | certain | Approval failed — try again. | `modules/hall-ui/public/pair.js:92` |
152
154
  | `0832ba017c90` | certain | Approve & re-run | `modules/hall-ui/public/gate.js:100` |
153
155
  | `222121f67525` | certain | Approve only if you just clicked “Connect” in the Dev Box app on your own machine. Approving signs that app in | `modules/hall-ui/public/pair.html:30` |
@@ -306,8 +308,8 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
306
308
  | `8b470cebf53d` | certain | Could not remove your key. Try again. | `modules/hall-ui/public/settings.js:1093` |
307
309
  | `4cf6bc8b377b` | certain | Could not remove. Try again. | `modules/hall-ui/public/settings.js:1092` |
308
310
  | `7a778d188e87` | certain | Could not remove: {…} | `modules/hall-ui/public/government.js:336` |
309
- | `ebeda72a6c15` | certain | Could not request access right now. | `modules/hall-ui/public/my-projects.js:140` |
310
- | `ebeda72a6c15` | certain | Could not request access right now. | `modules/hall-ui/public/my-projects.js:142` |
311
+ | `ebeda72a6c15` | certain | Could not request access right now. | `modules/hall-ui/public/my-projects.js:146` |
312
+ | `ebeda72a6c15` | certain | Could not request access right now. | `modules/hall-ui/public/my-projects.js:148` |
311
313
  | `bb5a7753e62b` | certain | Could not reset: {…} | `modules/hall-ui/public/government.js:275` |
312
314
  | `b526ee0b8c48` | certain | Could not save just now. Try again. | `modules/hall-ui/public/builders.js:459` |
313
315
  | `41a351aea8d4` | certain | Could not save the volume. Try again. | `modules/hall-ui/public/settings.js:214` |
@@ -338,7 +340,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
338
340
  | `278e8281c82f` | certain | Could not vote: {…} | `modules/hall-ui/public/government.js:913` |
339
341
  | `a48e9f098392` | certain | Could not {…} just now. | `modules/hall-ui/public/goals-render.js:951` |
340
342
  | `cb09ee26014f` | certain | Could not {…}: {…} | `modules/hall-ui/public/government.js:288` |
341
- | `7f600651bbf1` | certain | Couldn’t load your projects right now. | `modules/hall-ui/public/my-projects.js:115` |
343
+ | `7f600651bbf1` | certain | Couldn’t load your projects right now. | `modules/hall-ui/public/my-projects.js:117` |
342
344
  | `28ec06f6060a` | certain | Counted, not judged: each question is worth the same, and earns its share in proportion to how much real text | `modules/hall-ui/public/ideas.html:104` |
343
345
  | `38827a954f3b` | certain | Create a custom rank first. | `modules/hall-ui/public/government.js:419` |
344
346
  | `dd8ead7adc9a` | certain | Create rank | `modules/hall-ui/public/government.html:69` |
@@ -383,7 +385,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
383
385
  | `7f1b1c32d360` | certain | Each slash-command can run on a different model. The system has a default for every skill; choose another to o | `modules/hall-ui/public/settings.html:245` |
384
386
  | `854a21d2eb39` | certain | Eight achievements recognize sustained contribution. Earned ones are highlighted; the rest show what’s left to | `modules/hall-ui/public/hall-widgets.js:197` |
385
387
  | `58d381535285` | certain | Engagement reports | `modules/hall-ui/public/watch.html:120` |
386
- | `35d1ae09a6b3` | certain | Enter a project URL. | `modules/hall-ui/public/my-projects.js:122` |
388
+ | `35d1ae09a6b3` | certain | Enter a project URL. | `modules/hall-ui/public/my-projects.js:124` |
387
389
  | `bd7a40639a1c` | certain | Estimate (min) | `modules/hall-ui/public/task-detail.js:362` |
388
390
  | `c15d1f0ddfb6` | certain | Estimation accuracy | `modules/hall-ui/public/watch.html:133` |
389
391
  | `44f928d13699` | certain | Event sounds | `modules/hall-ui/public/settings.html:282` |
@@ -826,7 +828,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
826
828
  | `44579eef0c8d` | certain | Tasks — {{worldName}} | `modules/hall-ui/public/work.html:6` |
827
829
  | `44579eef0c8d` | certain | Tasks — {{worldName}} | `modules/hall-ui/public/work.html:6` |
828
830
  | `2f4f047dc1b4` | certain | Text size | `modules/hall-ui/public/shell.js:321` |
829
- | `4a8d1beb6f98` | certain | That project isn’t on the platform. | `modules/hall-ui/public/my-projects.js:139` |
831
+ | `4a8d1beb6f98` | certain | That project isn’t on the platform. | `modules/hall-ui/public/my-projects.js:145` |
830
832
  | `c0e28f30524d` | certain | That project's own standing, as reported by that project. | `modules/hall-ui/public/profile.js:230` |
831
833
  | `50198c465225` | certain | The 16-value rank enum in three bands, which ranks are live, and the permissions each rank holds. | `modules/hall-ui/public/diagrams.html:68` |
832
834
  | `1569b8838862` | certain | The Dev Box app now acts as | `modules/hall-ui/public/pair.js:40` |
@@ -3,9 +3,9 @@
3
3
  "generator": "scripts/gds/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 1533,
7
- "unique_ids": 1334,
8
- "certain": 1533,
6
+ "strings": 1535,
7
+ "unique_ids": 1336,
8
+ "certain": 1535,
9
9
  "heuristic": 0,
10
10
  "duplicate_ids": 154
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
15
  "files": 71,
16
- "strings": 1128
16
+ "strings": 1130
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -589,8 +589,8 @@
589
589
  "text": "Could not request access right now.",
590
590
  "occurrences": 2,
591
591
  "at": [
592
- "modules/hall-ui/public/my-projects.js:140",
593
- "modules/hall-ui/public/my-projects.js:142"
592
+ "modules/hall-ui/public/my-projects.js:146",
593
+ "modules/hall-ui/public/my-projects.js:148"
594
594
  ]
595
595
  },
596
596
  {
@@ -2102,6 +2102,15 @@
2102
2102
  "origin": "html-text",
2103
2103
  "confidence": "certain"
2104
2104
  },
2105
+ {
2106
+ "id": "b8476c677e7c",
2107
+ "surface": "builders-hall",
2108
+ "text": "Add a note (optional)",
2109
+ "file": "modules/hall-ui/public/my-projects.html",
2110
+ "line": 52,
2111
+ "origin": "html-text",
2112
+ "confidence": "certain"
2113
+ },
2105
2114
  {
2106
2115
  "id": "62eded408097",
2107
2116
  "surface": "builders-hall",
@@ -2219,6 +2228,15 @@
2219
2228
  "origin": "js-markup",
2220
2229
  "confidence": "certain"
2221
2230
  },
2231
+ {
2232
+ "id": "1ab0e6d5b079",
2233
+ "surface": "builders-hall",
2234
+ "text": "Anything the project's owner should know — what you'd like to work on, who you are.",
2235
+ "file": "modules/hall-ui/public/my-projects.html",
2236
+ "line": 54,
2237
+ "origin": "html-attr:placeholder",
2238
+ "confidence": "certain"
2239
+ },
2222
2240
  {
2223
2241
  "id": "c89833aa985a",
2224
2242
  "surface": "builders-hall",
@@ -3646,7 +3664,7 @@
3646
3664
  "surface": "builders-hall",
3647
3665
  "text": "Could not request access right now.",
3648
3666
  "file": "modules/hall-ui/public/my-projects.js",
3649
- "line": 140,
3667
+ "line": 146,
3650
3668
  "origin": "js-text-assign",
3651
3669
  "confidence": "certain"
3652
3670
  },
@@ -3655,7 +3673,7 @@
3655
3673
  "surface": "builders-hall",
3656
3674
  "text": "Could not request access right now.",
3657
3675
  "file": "modules/hall-ui/public/my-projects.js",
3658
- "line": 142,
3676
+ "line": 148,
3659
3677
  "origin": "js-text-assign",
3660
3678
  "confidence": "certain"
3661
3679
  },
@@ -3934,7 +3952,7 @@
3934
3952
  "surface": "builders-hall",
3935
3953
  "text": "Couldn’t load your projects right now.",
3936
3954
  "file": "modules/hall-ui/public/my-projects.js",
3937
- "line": 115,
3955
+ "line": 117,
3938
3956
  "origin": "js-text-assign",
3939
3957
  "confidence": "certain"
3940
3958
  },
@@ -4339,7 +4357,7 @@
4339
4357
  "surface": "builders-hall",
4340
4358
  "text": "Enter a project URL.",
4341
4359
  "file": "modules/hall-ui/public/my-projects.js",
4342
- "line": 122,
4360
+ "line": 124,
4343
4361
  "origin": "js-text-assign",
4344
4362
  "confidence": "certain"
4345
4363
  },
@@ -8326,7 +8344,7 @@
8326
8344
  "surface": "builders-hall",
8327
8345
  "text": "That project isn’t on the platform.",
8328
8346
  "file": "modules/hall-ui/public/my-projects.js",
8329
- "line": 139,
8347
+ "line": 145,
8330
8348
  "origin": "js-text-assign",
8331
8349
  "confidence": "certain"
8332
8350
  },
@@ -37,4 +37,26 @@
37
37
  background: var(--surface);
38
38
  color: var(--ink);
39
39
  }
40
+ /* The application's optional note (task 1002330). Sits under the one-click join
41
+ row so the click stays the primary act and the note is plainly skippable. */
42
+ .join-note-label {
43
+ display: block;
44
+ margin: 10px 0 4px;
45
+ color: var(--ink-soft);
46
+ font-size: 12px;
47
+ }
48
+ .join-note {
49
+ display: block;
50
+ width: 100%;
51
+ box-sizing: border-box;
52
+ font: inherit;
53
+ font-size: 13px;
54
+ line-height: 1.4;
55
+ padding: 6px 10px;
56
+ resize: vertical;
57
+ border: 1px solid var(--border-strong);
58
+ border-radius: var(--radius-sm);
59
+ background: var(--surface);
60
+ color: var(--ink);
61
+ }
40
62
  .note { margin: 10px 0 0; color: var(--ink-soft); font-size: 13px; min-height: 1.2em; }
@@ -46,7 +46,13 @@
46
46
  <input id="join-origin" type="url" placeholder="https://project.cloudbongos.com" autocomplete="off">
47
47
  <button id="join-btn" type="button" class="btn-ghost">Request to join</button>
48
48
  </div>
49
- <p id="join-note" class="note"></p>
49
+ <!-- An application is one click; the note is the applicant's own words,
50
+ and it is optional on purpose (task 1002330 — lightweight, never an
51
+ owner-defined form). It reaches the project owner's queue. -->
52
+ <label class="join-note-label" for="join-note">Add a note (optional)</label>
53
+ <textarea id="join-note" class="join-note" rows="2" maxlength="400"
54
+ placeholder="Anything the project's owner should know — what you'd like to work on, who you are."></textarea>
55
+ <p id="join-status" class="note"></p>
50
56
  </section>
51
57
  </main>
52
58
 
@@ -1,6 +1,7 @@
1
1
  // my-projects.js — the signed-in builder's cross-project view (ADR 0141 §3, task
2
2
  // #1002112). Fetches /api/gds/my-projects (session cookie, same-origin) and renders
3
- // owned + building-on lists with self-service leave, plus a join-by-origin form.
3
+ // owned + building-on lists with self-service leave, plus a join-by-origin form
4
+ // whose application is one click plus an optional note (task 1002330).
4
5
  // All network values are rendered via textContent — never innerHTML.
5
6
  (function () {
6
7
  'use strict';
@@ -8,7 +9,8 @@
8
9
  var buildingEl = document.getElementById('building');
9
10
  var joinBtn = document.getElementById('join-btn');
10
11
  var joinInput = document.getElementById('join-origin');
11
- var joinNote = document.getElementById('join-note');
12
+ var joinNoteInput = document.getElementById('join-note'); // the applicant's own words (optional)
13
+ var joinStatus = document.getElementById('join-status'); // what the project answered
12
14
 
13
15
  function el(tag, cls) { var e = document.createElement(tag); if (cls) e.className = cls; return e; }
14
16
 
@@ -119,27 +121,31 @@
119
121
 
120
122
  joinBtn.addEventListener('click', function () {
121
123
  var origin = (joinInput.value || '').trim();
122
- if (!origin) { joinNote.textContent = 'Enter a project URL.'; return; }
124
+ if (!origin) { joinStatus.textContent = 'Enter a project URL.'; return; }
125
+ var note = (joinNoteInput.value || '').trim();
123
126
  joinBtn.disabled = true;
124
- joinNote.textContent = 'Requesting…';
127
+ joinStatus.textContent = 'Requesting…';
125
128
  fetch('/api/gds/my-projects/join', {
126
129
  method: 'POST',
127
130
  headers: { 'Content-Type': 'application/json' },
128
- body: JSON.stringify({ origin: origin }),
131
+ /* note is omitted entirely when blank — an application with nothing written
132
+ is still an application (task 1002330). */
133
+ body: JSON.stringify(note ? { origin: origin, note: note } : { origin: origin }),
129
134
  })
130
135
  .then(function (r) { return r.json().then(function (b) { return { ok: r.ok, body: b }; }); })
131
136
  .then(function (res) {
132
137
  joinBtn.disabled = false;
133
138
  if (res.ok && res.body && res.body.message) {
134
- joinNote.textContent = res.body.message;
135
- /* a door refusal rides a 200 (task 1003044): keep the address, and don't repaint as if a request went out */
139
+ joinStatus.textContent = res.body.message;
140
+ /* a door refusal rides a 200 (task 1003044): keep the address AND the
141
+ note the applicant wrote, and don't repaint as if a request went out */
136
142
  var refused = res.body.status === 'invite_only' || res.body.status === 'open_enrollment' || res.body.status === 'members_only';
137
- if (!refused) { joinInput.value = ''; load(); }
143
+ if (!refused) { joinInput.value = ''; joinNoteInput.value = ''; load(); }
138
144
  }
139
- else if (res.body && res.body.error && res.body.error.code === 'unknown_project') { joinNote.textContent = 'That project isn’t on the platform.'; }
140
- else { joinNote.textContent = 'Could not request access right now.'; }
145
+ else if (res.body && res.body.error && res.body.error.code === 'unknown_project') { joinStatus.textContent = 'That project isn’t on the platform.'; }
146
+ else { joinStatus.textContent = 'Could not request access right now.'; }
141
147
  })
142
- .catch(function () { joinBtn.disabled = false; joinNote.textContent = 'Could not request access right now.'; });
148
+ .catch(function () { joinBtn.disabled = false; joinStatus.textContent = 'Could not request access right now.'; });
143
149
  });
144
150
 
145
151
  load();
@@ -2,7 +2,9 @@
2
2
  // project "My Projects" view + self-service join / leave (ADR 0141 §3, task #1002112).
3
3
  //
4
4
  // GET /my-projects — projects this account builds on, owned grouped apart
5
- // POST /my-projects/join — request to join a project (executed by the project)
5
+ // POST /my-projects/join — apply to a project: one click plus an optional
6
+ // note in the applicant's own words (task 1002330).
7
+ // Relayed; the project executes and decides.
6
8
  // POST /my-projects/:clientId/leave — self-service offboard (reflected to the hub)
7
9
  //
8
10
  // All gated on a live HUB session (requireBuilder). Authority stays LOCAL: JOIN only
@@ -44,6 +46,40 @@ function joinRelayOutcome(status, body) {
44
46
  return { status: outcome, message: RELAY_SENTENCE[outcome] };
45
47
  }
46
48
 
49
+ // The provenance line the project's owner reads when an application arrives
50
+ // through the platform rather than off their own landing page. It is the HUB's
51
+ // sentence, not the applicant's — see relayApplicationNote.
52
+ const RELAY_PROVENANCE = 'via platform My Projects';
53
+
54
+ // What an applicant may write, and what the project will store. The project's
55
+ // public POST /access-requests caps `note` at 500 chars and `display_name` at
56
+ // 100, and refuses anything longer with a 400 — which joinRelayOutcome cannot
57
+ // read, so the hub would tell the applicant the project "could not be reached".
58
+ // A lie about someone else's door because our own field was too long is the bug
59
+ // these two caps exist to prevent: the hub asks for less than the project takes,
60
+ // and clamps anyway.
61
+ const APPLICANT_NOTE_MAX = 400;
62
+ const APPLICANT_NAME_MAX = 100;
63
+
64
+ // relayApplicationNote — the note that reaches the project's queue: the
65
+ // applicant's own words plus where the request came from (task 1002330). An
66
+ // application with nothing written is still an application, so an absent note
67
+ // relays the provenance line alone — exactly what this route sent before the
68
+ // applicant could write anything. Pure; clamped so the composed string always
69
+ // fits the project's 500-char column.
70
+ function relayApplicationNote(note) {
71
+ const written = String(note == null ? '' : note).trim().slice(0, APPLICANT_NOTE_MAX);
72
+ return written ? `${written} (${RELAY_PROVENANCE})` : RELAY_PROVENANCE;
73
+ }
74
+
75
+ // relayApplicantName — the applicant's display name as the project should show
76
+ // it, or null when they have none (the queue then reads as the bare @login it
77
+ // always did). Clamped to the project's column.
78
+ function relayApplicantName(displayName) {
79
+ const name = String(displayName == null ? '' : displayName).trim().slice(0, APPLICANT_NAME_MAX);
80
+ return name || null;
81
+ }
82
+
47
83
  module.exports = function myProjectsRoutes() {
48
84
  const router = express.Router();
49
85
 
@@ -75,7 +111,10 @@ module.exports = function myProjectsRoutes() {
75
111
  // stays local). SSRF guard: only relay to an origin that is a REGISTERED, active
76
112
  // client — never an arbitrary builder-supplied URL.
77
113
  router.post('/my-projects/join', requireBuilder, async (req, res) => {
78
- if (validateOrRespond(req, res, { origin: { required: true, type: 'string', maxLength: 512 } })) return;
114
+ if (validateOrRespond(req, res, {
115
+ origin: { required: true, type: 'string', maxLength: 512 },
116
+ note: { type: 'string', maxLength: APPLICANT_NOTE_MAX },
117
+ })) return;
79
118
  try {
80
119
  const client = await pi.getClientByOrigin(req.body.origin, { pool });
81
120
  if (!client || client.status !== 'active' || !client.origin) {
@@ -89,7 +128,11 @@ module.exports = function myProjectsRoutes() {
89
128
  const r = await fetch(`${client.origin}/api/gds/access-requests`, {
90
129
  method: 'POST',
91
130
  headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'User-Agent': userAgent() },
92
- body: JSON.stringify({ github_login: req.builder.github_login, note: 'via platform My Projects' }),
131
+ body: JSON.stringify({
132
+ github_login: req.builder.github_login,
133
+ display_name: relayApplicantName(req.builder.display_name),
134
+ note: relayApplicationNote(req.body.note),
135
+ }),
93
136
  });
94
137
  const body = await r.json().catch(() => null);
95
138
  outcome = joinRelayOutcome(r.status, body);
@@ -164,3 +207,7 @@ module.exports = function myProjectsRoutes() {
164
207
  return router;
165
208
  };
166
209
  module.exports.joinRelayOutcome = joinRelayOutcome;
210
+ module.exports.relayApplicationNote = relayApplicationNote;
211
+ module.exports.relayApplicantName = relayApplicantName;
212
+ module.exports.APPLICANT_NOTE_MAX = APPLICANT_NOTE_MAX;
213
+
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.395",
3
+ "version": "1.19.396",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.395",
9
+ "version": "1.19.396",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.395",
3
+ "version": "1.19.396",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
package/src/module-api.js CHANGED
@@ -1230,8 +1230,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1230
1230
  // landed since 1.19.393 with no explicit bump. run 33182535783. (task 1002620)
1231
1231
  // 1.19.395 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1232
1232
  // landed since 1.19.394 with no explicit bump. run 33188021384. (task 1002620)
1233
+ // 1.19.396 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1234
+ // landed since 1.19.395 with no explicit bump. run 33188525568. (task 1002620)
1233
1235
  // ---------------------------------------------------------------------------
1234
- const CORE_VERSION = '1.19.395'; // CI auto-patch carrier (ADR 0161); see changelog above
1236
+ const CORE_VERSION = '1.19.396'; // CI auto-patch carrier (ADR 0161); see changelog above
1235
1237
 
1236
1238
  // A namespaced logger so a module's log lines are attributable + consistent.
1237
1239
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,112 @@
1
+ // tests/join_application_note.mjs — a project-level application is one click plus
2
+ // an optional note (task 1002330, ADR 0182 D6 / the admin-console spec's D2).
3
+ //
4
+ // The hub does not admit anyone; it relays the application to the project's own
5
+ // public POST /access-requests, which is what the owner's queue reads. So what
6
+ // this file pins is the composition of that relayed row:
7
+ //
8
+ // - the applicant's own words reach the owner, with the hub's provenance line
9
+ // saying where the request came from;
10
+ // - writing nothing is still an application — the pre-1002330 behaviour;
11
+ // - nothing the applicant types can push the relayed fields past the project's
12
+ // own column caps (500 note / 100 display_name), because a 400 from the
13
+ // project is a status joinRelayOutcome cannot read, and the applicant would
14
+ // then be told the project "could not be reached" — a lie about someone
15
+ // else's door caused by our own field.
16
+ //
17
+ // Pure, DB-free.
18
+ //
19
+ // Run: node --test tests/join_application_note.mjs
20
+
21
+ import assert from 'node:assert/strict';
22
+ import { test } from 'node:test';
23
+ import {
24
+ relayApplicationNote,
25
+ relayApplicantName,
26
+ APPLICANT_NOTE_MAX,
27
+ } from '../modules/platform-identity/routes/my-projects.js';
28
+
29
+ // The caps the PROJECT's public POST /access-requests enforces on the row it
30
+ // stores (modules/onboarding/routes/access-requests.js). Restated here so this
31
+ // test fails if the hub ever starts asking for more than a project will take.
32
+ const PROJECT_NOTE_MAX = 500;
33
+ const PROJECT_NAME_MAX = 100;
34
+
35
+ test('the applicant’s own words reach the owner, with where it came from', () => {
36
+ const note = relayApplicationNote("I'd like to help with the terrain work");
37
+ assert.match(note, /I'd like to help with the terrain work/);
38
+ assert.match(note, /via platform My Projects/);
39
+ });
40
+
41
+ test('an application with nothing written is still an application', () => {
42
+ for (const empty of [undefined, null, '', ' ', '\n\t']) {
43
+ assert.equal(relayApplicationNote(empty), 'via platform My Projects',
44
+ `${JSON.stringify(empty)} relays the provenance line alone`);
45
+ }
46
+ });
47
+
48
+ test('the composed note always fits the project’s column', () => {
49
+ const composed = relayApplicationNote('x'.repeat(5000));
50
+ assert.ok(composed.length <= PROJECT_NOTE_MAX,
51
+ `composed note is ${composed.length} chars, project stores ${PROJECT_NOTE_MAX}`);
52
+ assert.match(composed, /via platform My Projects/, 'provenance survives the clamp');
53
+ });
54
+
55
+ test('the hub asks for less note than the project will take', () => {
56
+ const suffix = relayApplicationNote('').length + ' ()'.length;
57
+ assert.ok(APPLICANT_NOTE_MAX + suffix <= PROJECT_NOTE_MAX,
58
+ 'the validator cap plus the provenance suffix must fit the project’s 500');
59
+ });
60
+
61
+ test('a display name travels so the queue reads as a person, not a bare login', () => {
62
+ assert.equal(relayApplicantName(' Jane Doe '), 'Jane Doe');
63
+ const clamped = relayApplicantName('n'.repeat(400));
64
+ assert.ok(clamped.length <= PROJECT_NAME_MAX);
65
+ });
66
+
67
+ test('no display name relays null — the queue keeps the bare @login it always had', () => {
68
+ for (const empty of [undefined, null, '', ' ']) {
69
+ assert.equal(relayApplicantName(empty), null, `${JSON.stringify(empty)}`);
70
+ }
71
+ });
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // The wiring — that the composed note actually travels, and that the applicant's
75
+ // words survive a door refusal. Source assertions (the house pattern in
76
+ // tests/joinability_invite_only.mjs): the relay needs a live hub session and a
77
+ // second running project to exercise, which no unit test can boot.
78
+ // ---------------------------------------------------------------------------
79
+
80
+ import fs from 'node:fs';
81
+ import path from 'node:path';
82
+ import { fileURLToPath } from 'node:url';
83
+
84
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
85
+ const src = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
86
+
87
+ test('the relay sends the composed note and the applicant’s name — not a constant', () => {
88
+ const route = src('modules/platform-identity/routes/my-projects.js');
89
+ const join = (route.match(/router\.post\('\/my-projects\/join'[\s\S]*?\n \}\);\n/) || [''])[0];
90
+ assert.ok(join, 'the join route must exist');
91
+ assert.match(join, /note: relayApplicationNote\(req\.body\.note\)/, 'the applicant’s words are composed, then relayed');
92
+ assert.match(join, /display_name: relayApplicantName\(req\.builder\.display_name\)/);
93
+ assert.doesNotMatch(join, /note: 'via platform My Projects'/, 'the hardcoded note is gone');
94
+ assert.match(join, /note: \{ type: 'string', maxLength: APPLICANT_NOTE_MAX \}/, 'the note is declared in the route’s schema');
95
+ });
96
+
97
+ test('the join box offers the note as optional, and never demands it', () => {
98
+ const html = src('modules/hall-ui/public/my-projects.html');
99
+ assert.match(html, /<textarea id="join-note"/, 'the note field exists');
100
+ assert.match(html, /maxlength="400"/, 'the field cannot outrun the route’s cap');
101
+ assert.match(html, /Add a note \(optional\)/, 'and it is labelled optional');
102
+ assert.match(html, /id="join-status"/, 'the answer line is its own element, not the note field');
103
+ });
104
+
105
+ test('a door refusal keeps what the applicant typed; a filed request clears it', () => {
106
+ const js = src('modules/hall-ui/public/my-projects.js');
107
+ const click = (js.match(/joinBtn\.addEventListener\('click'[\s\S]*?\n \}\);\n/) || [''])[0];
108
+ assert.ok(click, 'the join handler must exist');
109
+ assert.match(click, /note \? \{ origin: origin, note: note \} : \{ origin: origin \}/, 'a blank note is omitted, not sent as ""');
110
+ assert.match(click, /if \(!refused\) \{ joinInput\.value = ''; joinNoteInput\.value = ''; load\(\); \}/,
111
+ 'cleared only when the request was actually filed — a refusal keeps the words for the next try');
112
+ });