@bongos/core 1.24.74 → 1.24.75

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/bongos/package-core.js",
5
- "core_version": "1.24.74",
6
- "core_contract": "1.24.74",
7
- "source_commit": "a6d60eb20955aa0e73a134ac7e441ac09b196f0b",
5
+ "core_version": "1.24.75",
6
+ "core_contract": "1.24.75",
7
+ "source_commit": "059ee68ec238feb574a12809820c2715dca62863",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-11-09T22:49:22.010Z",
9
+ "built_at": "2026-11-09T23:43:21.612Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 639,
13
13
  "agent_docs_stubbed": 32,
14
- "functional_verbatim": 3913,
14
+ "functional_verbatim": 3915,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 4585,
20
- "tree_sha256": "68023229080056ba5c8a6706afcfd35293133d7dfeea5093194ebea8f4b351c1",
19
+ "file_count": 4587,
20
+ "tree_sha256": "25f2b339c028a4f3f6251cb230dcc21dd102eaa2976b4e16dd3c118e39fd55a4",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2462,12 +2462,12 @@
2462
2462
  {
2463
2463
  "path": "docs/api-reference.md",
2464
2464
  "mode": "0000644",
2465
- "sha256": "4a563b87dbe8c974763e920431e9c6fec4c67ab9be4d6b8347e04660ec5a4825"
2465
+ "sha256": "f862a7f8539dff88cb7b381a934e802ac897482aa0152ce175d653999292f71d"
2466
2466
  },
2467
2467
  {
2468
2468
  "path": "docs/api/openapi.json",
2469
2469
  "mode": "0000644",
2470
- "sha256": "c1a219ca2fb09e5488269d97f7dfa7785c1929bf8f71a421f7d7dacb11ffa131"
2470
+ "sha256": "c2f6101d974533f5ca071132625a44ac50f5f13adea8f62833cde2a319ab6c03"
2471
2471
  },
2472
2472
  {
2473
2473
  "path": "docs/architecture.md",
@@ -2492,12 +2492,12 @@
2492
2492
  {
2493
2493
  "path": "docs/copy-inventory.md",
2494
2494
  "mode": "0000644",
2495
- "sha256": "4fc26e475163cfd03791cdb6aa548478b573c39b1ce8c72e6d1245dff8ffa678"
2495
+ "sha256": "ffb9813d83728071c19eb73692a6721530047e4773fa17ef724f41b1d6aa4726"
2496
2496
  },
2497
2497
  {
2498
2498
  "path": "docs/copy-registry.json",
2499
2499
  "mode": "0000644",
2500
- "sha256": "e4a93ee08874c356a1d884ebacf9608a242d274383b65e1997ad0e73cc6d7f81"
2500
+ "sha256": "7cbf7c15e7201b681f3850726f75e867348f13669dcc400c7907d58ecd272bf6"
2501
2501
  },
2502
2502
  {
2503
2503
  "path": "docs/design/apex-pass-2-direction.md",
@@ -3007,7 +3007,7 @@
3007
3007
  {
3008
3008
  "path": "docs/module-api-changelog.md",
3009
3009
  "mode": "0000644",
3010
- "sha256": "8e113be80df86f1f18cfea88430a8a1c889c66407f8ea0a0510e92b77c2f5b9a"
3010
+ "sha256": "d8ae77d616c3b19e499fa5a111386e5124ba6700526068fd64fe5d293834fc5b"
3011
3011
  },
3012
3012
  {
3013
3013
  "path": "docs/modules-contract.md",
@@ -3097,7 +3097,7 @@
3097
3097
  {
3098
3098
  "path": "docs/page-readings.json",
3099
3099
  "mode": "0000644",
3100
- "sha256": "46a1626b9583eadadc9e4a7579ca58a9cb3e0b45c0ad19e106e008e22a1ff7ad"
3100
+ "sha256": "656c3ae3975c2efbb924dd4264695814665fdc3c7ca6519f9c7415a8bbca2421"
3101
3101
  },
3102
3102
  {
3103
3103
  "path": "docs/project-context.template.md",
@@ -5432,7 +5432,7 @@
5432
5432
  {
5433
5433
  "path": "modules/government/board.js",
5434
5434
  "mode": "0000644",
5435
- "sha256": "261a349be2bb554e000c26ab04e33e72a0c244b1f76fb0271341166a390f9924"
5435
+ "sha256": "e33aeffe12d18670d435dfcbf5c779c6853e8ef59c1506f81d23f9e829671642"
5436
5436
  },
5437
5437
  {
5438
5438
  "path": "modules/government/build-rights.js",
@@ -5472,7 +5472,7 @@
5472
5472
  {
5473
5473
  "path": "modules/government/db.js",
5474
5474
  "mode": "0000644",
5475
- "sha256": "ae3f0f06ce140184a22a25b4883fb2fa0620cb4e03259af84a580ba0bf9306cf"
5475
+ "sha256": "5a28251b7f986861911233f1a5c3286857e8ccbef061e63697277bac29d759b9"
5476
5476
  },
5477
5477
  {
5478
5478
  "path": "modules/government/decision-board.js",
@@ -5499,6 +5499,11 @@
5499
5499
  "mode": "0000644",
5500
5500
  "sha256": "cbc4de83d3b498772fc601a4c77531e269d819d91e2f70e86ebf845b55c9d4e1"
5501
5501
  },
5502
+ {
5503
+ "path": "modules/government/genesis-close.js",
5504
+ "mode": "0000644",
5505
+ "sha256": "8fa8b9d422e75bd32feeab760d1a21c6935d305dee1dc818e3d2e2d3112ecc4a"
5506
+ },
5502
5507
  {
5503
5508
  "path": "modules/government/law-board.js",
5504
5509
  "mode": "0000644",
@@ -5739,6 +5744,11 @@
5739
5744
  "mode": "0000644",
5740
5745
  "sha256": "f09c9f6492a9607266bbaa0d235f2aaa9779500408c20abc7b0049df22592993"
5741
5746
  },
5747
+ {
5748
+ "path": "modules/government/migrations/government_038_vote_cast_via.sql",
5749
+ "mode": "0000644",
5750
+ "sha256": "c102d89b2fb4a9764333b9f4b1f1601c9e34e28fe8926b9d767889ebe6677d8b"
5751
+ },
5742
5752
  {
5743
5753
  "path": "modules/government/module.json",
5744
5754
  "mode": "0000644",
@@ -5792,7 +5802,7 @@
5792
5802
  {
5793
5803
  "path": "modules/government/routes/government.js",
5794
5804
  "mode": "0000644",
5795
- "sha256": "fe2dbd8f6056757b7734c0a093779db83dc43fa386ba1dc29a04f6d3efaf7b1b"
5805
+ "sha256": "91fed3be33455271af665861537082345152fa3a6a27917a1cf9bd847c701561"
5796
5806
  },
5797
5807
  {
5798
5808
  "path": "modules/government/routes/rank-sync.js",
@@ -6087,7 +6097,7 @@
6087
6097
  {
6088
6098
  "path": "modules/hall-ui/public/board-room.js",
6089
6099
  "mode": "0000644",
6090
- "sha256": "9d0e498f19139d151374ae0ee7ee8931fa0bb2d407e198fc48b06be5bc753fb0"
6100
+ "sha256": "aa65fa3d394e3c66a21f9e289aeb187d51d541ef270d63626b9bb69b5e9e5950"
6091
6101
  },
6092
6102
  {
6093
6103
  "path": "modules/hall-ui/public/board-room.states.json",
@@ -6347,7 +6357,7 @@
6347
6357
  {
6348
6358
  "path": "modules/hall-ui/public/genesis-home.js",
6349
6359
  "mode": "0000644",
6350
- "sha256": "bb623bb0e9988f78ab81ea593d5e3851057f80b5fe6604df33ab83146618422c"
6360
+ "sha256": "ae19fff3052ac92cf01e981ece176cc4bf3ed6458c752e9242bce8b94b40a31a"
6351
6361
  },
6352
6362
  {
6353
6363
  "path": "modules/hall-ui/public/genesis/r5-life-1-primordial-sea-cut.webp",
@@ -8982,7 +8992,7 @@
8982
8992
  {
8983
8993
  "path": "modules/onboarding/genesis-home.js",
8984
8994
  "mode": "0000644",
8985
- "sha256": "79a712a6dc1ed7633f25b9558ddca0ca75ee5eba0b996dc5a4a57bb57cfc5356"
8995
+ "sha256": "9b258dee07b53441096342328e9f59832094290dbeed5431e941a5589c373250"
8986
8996
  },
8987
8997
  {
8988
8998
  "path": "modules/onboarding/howto-guide.js",
@@ -9037,7 +9047,7 @@
9037
9047
  {
9038
9048
  "path": "modules/onboarding/routes/onboarding.js",
9039
9049
  "mode": "0000644",
9040
- "sha256": "011dfc781cbe8c7a17956fa2020e0327cf0e46f537a9057cf2b4911752ba813a"
9050
+ "sha256": "c2ef042a6227c5ae790efc1693fbbb85058a3a023ddc7fde279e45f070f03fdf"
9041
9051
  },
9042
9052
  {
9043
9053
  "path": "modules/page-notes/CLAUDE.md",
@@ -11927,12 +11937,12 @@
11927
11937
  {
11928
11938
  "path": "package-lock.json",
11929
11939
  "mode": "0000644",
11930
- "sha256": "c22d51b353240896e20ad2a4caadd2e10afe31209a47ec40f7ff67e968722b3c"
11940
+ "sha256": "489befbacb6d245153c3c4db2fb0d5b99144458f67fe7d59d274eb9629bb6636"
11931
11941
  },
11932
11942
  {
11933
11943
  "path": "package.json",
11934
11944
  "mode": "0000644",
11935
- "sha256": "9d8da0bbf818deb5ac50bbb6062eddfccfdc1db5b9735a1763e3b1c49ccf128d"
11945
+ "sha256": "797dc5a7a1f6421778c1ae74e6e34deb098bb09d0319e0fd22ea0864d57f0631"
11936
11946
  },
11937
11947
  {
11938
11948
  "path": "public-docs/index.html",
@@ -11952,7 +11962,7 @@
11952
11962
  {
11953
11963
  "path": "release-notes.json",
11954
11964
  "mode": "0000644",
11955
- "sha256": "ce71a4e4221b4b16ae4108ccb10bbe5309d266ca86e2ef1c558f93454ea8bbf5"
11965
+ "sha256": "633a514f8be10e412f2e4482bae80a1b9470def0cd07c02d25863c511b4804c7"
11956
11966
  },
11957
11967
  {
11958
11968
  "path": "scripts/bongos/CLAUDE.md",
@@ -12612,7 +12622,7 @@
12612
12622
  {
12613
12623
  "path": "scripts/bongos/government-walk.js",
12614
12624
  "mode": "0000644",
12615
- "sha256": "2447ccaae2dbdfbbf88feb170bfdcfdaf3ff7b0a6e45060941fa78a4b39be7c2"
12625
+ "sha256": "7f959c5dc7c2530bbd89686a269e4d545a47deeb407eeac9f3aff8908dbab32c"
12616
12626
  },
12617
12627
  {
12618
12628
  "path": "scripts/bongos/grade-correlation-audit.js",
@@ -15907,7 +15917,7 @@
15907
15917
  {
15908
15918
  "path": "src/module-api.js",
15909
15919
  "mode": "0000644",
15910
- "sha256": "8c9e633d0a366d3fb5ce27fa25323dde9dbf7fe45a9f2ca2a3e0d80e465a816c"
15920
+ "sha256": "9b784cf80bdf691b55cc24ef9f11e45be78d517fd680a6b3680412d29d17ba90"
15911
15921
  },
15912
15922
  {
15913
15923
  "path": "src/module-loader/catalog.js",
@@ -17902,7 +17912,7 @@
17902
17912
  {
17903
17913
  "path": "tests/genesis_home.mjs",
17904
17914
  "mode": "0000644",
17905
- "sha256": "6cadbdc1c3e1a276f2d381f7256d483fda6e07d8cfab1d7ac89837f2b0d64ffd"
17915
+ "sha256": "1ddd0386a7f4c2d72eff26f690f899b29e2c9a40590d3c91010b45d1247aa6b8"
17906
17916
  },
17907
17917
  {
17908
17918
  "path": "tests/genesis_life_plates.mjs",
@@ -18287,7 +18297,7 @@
18287
18297
  {
18288
18298
  "path": "tests/government_board_genesis_stage.mjs",
18289
18299
  "mode": "0000644",
18290
- "sha256": "a477c3f0aa0ab45167ed33a3452a8d9075a5a01a3d8722a1c290165b09ec08cf"
18300
+ "sha256": "03ed8afc02443616d674f1a6d78c4ef2a3ad33391d8a7d62f44cf51b5d09a1e7"
18291
18301
  },
18292
18302
  {
18293
18303
  "path": "tests/government_board_hash_redirect.mjs",
@@ -18512,7 +18522,7 @@
18512
18522
  {
18513
18523
  "path": "tests/government_seed.mjs",
18514
18524
  "mode": "0000644",
18515
- "sha256": "32e5559d53732662f8a2ab8f36a36c1633b62d1171ac148fe7e6a313ccfecf1b"
18525
+ "sha256": "52f56724cf29083659326bd383534c2bb929654e75e780c8812dc9650efab6fa"
18516
18526
  },
18517
18527
  {
18518
18528
  "path": "tests/government_session_scope.mjs",
@@ -7462,7 +7462,7 @@
7462
7462
  "founding"
7463
7463
  ],
7464
7464
  "summary": "POST /founding/stages/:stage/close",
7465
- "description": "**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
7465
+ "description": "task 1005121: one call. 200 { closed: true, next_stage } when the stage closed; 202 { ok: false, closed: false, reason: 'sitting_open', waiting, next } when a real sitting is deciding it — never a success for a stage that did not close.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
7466
7466
  "x-rank": "any-builder",
7467
7467
  "x-source": "modules/onboarding/routes/onboarding.js",
7468
7468
  "parameters": [
@@ -317,7 +317,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
317
317
  | GET | `/api/bongos/founding/builders` | `any-builder` | — | The founding mark (task 1004424): who joined while the project was founding, in join order, with the stage each joined at. |
318
318
  | GET | `/api/bongos/founding/genesis` | `any-builder` | — | A project born through the startup flow enters genesis on its first founding read (founding-birth.js, task 1004506) — here and on GET /me… |
319
319
  | POST | `/api/bongos/founding/plan` | `any-builder` | `days`, `passes`, `advisors`, `government`, `expected_builders`, `existing` | The plan's ranges and vocabularies are genesis-home.js parsePlan's (one source); this schema only names the fields so an unknown one is r… |
320
- | POST | `/api/bongos/founding/stages/:stage/close` | `any-builder` | — | |
320
+ | POST | `/api/bongos/founding/stages/:stage/close` | `any-builder` | — | task 1005121: one call. |
321
321
  | POST | `/api/bongos/founding/start` | `any-builder` | — | |
322
322
 
323
323
  ## `github` (6)
@@ -95,7 +95,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
95
95
  | `27d9721f358f` | certain | + Invite | `modules/hall-ui/public/goals-render.js:1213` |
96
96
  | `74815c8882ba` | certain | + send a new scout | `modules/hall-ui/public/market-watch.html:54` |
97
97
  | `646fab1157fe` | certain | + {…} more project{…} not publicly listed. | `modules/hall-ui/public/profile.js:394` |
98
- | `9c8aba2b2fa4` | certain | , // membership | `modules/hall-ui/public/board-room.js:316` |
98
+ | `9c8aba2b2fa4` | certain | , // membership | `modules/hall-ui/public/board-room.js:317` |
99
99
  | `37964829d693` | certain | , and | `modules/hall-ui/public/government.html:55` |
100
100
  | `ac57fe1ccd6e` | certain | , follow its acceptance criteria, stay within touches[], /builder-ship | `modules/hall-ui/public/bulk-toolbar.js:78` |
101
101
  | `574acf2f93bb` | certain | , never quietly ratified — so a clock is worth proposing. The recommended sitting is | `modules/hall-ui/public/government.js:651` |
@@ -427,7 +427,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
427
427
  | `e65b392144ac` | certain | Could not set up storage. Try again. | `modules/hall-ui/public/settings.js:853` |
428
428
  | `d4601887a058` | certain | Could not sign out everywhere: {…} | `modules/hall-ui/public/settings-sessions.js:110` |
429
429
  | `3db890b074a7` | certain | Could not update visibility just now. | `modules/hall-ui/public/goals-render.js:1365` |
430
- | `278e8281c82f` | certain | Could not vote: {…} | `modules/hall-ui/public/board-room.js:427` |
430
+ | `278e8281c82f` | certain | Could not vote: {…} | `modules/hall-ui/public/board-room.js:428` |
431
431
  | `a48e9f098392` | certain | Could not {…} just now. | `modules/hall-ui/public/goals-render.js:1351` |
432
432
  | `cb09ee26014f` | certain | Could not {…}: {…} | `modules/hall-ui/public/government.js:360` |
433
433
  | `f53f7aaa189f` | certain | Couldn’t load this goal’s task dependencies right now. | `modules/hall-ui/public/goal-forest-render.js:120` |
@@ -635,7 +635,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
635
635
  | `2170b74c6526` | certain | Isometric map of the repository | `modules/hall-ui/public/atlas.html:52` |
636
636
  | `a4631fe26669` | certain | It already exists, so gather what it has first | `modules/hall-ui/public/genesis-home.js:124` |
637
637
  | `36632d62169d` | certain | It became {…} {…} | `modules/hall-ui/public/studio.js:1210` |
638
- | `bd8d04eb265c` | certain | It cleared the completeness grade at {…}/100. | `modules/hall-ui/public/board-room.js:342` |
638
+ | `bd8d04eb265c` | certain | It cleared the completeness grade at {…}/100. | `modules/hall-ui/public/board-room.js:343` |
639
639
  | `bef8dbf78170` | certain | It comes back to your studio once it is applied, and nothing ships until you have seen it. | `modules/hall-ui/public/tweak-editor.html:97` |
640
640
  | `4f5b39802b07` | certain | It holds no unfinished work, so this is a single step. Archiving is reversible — an archived goal can be reope | `modules/hall-ui/public/goals-render.js:1029` |
641
641
  | `dbd497dba4c0` | certain | It is waiting on {…} {…}. It can be claimed once {…}. | `modules/hall-ui/public/task.js:769` |
@@ -644,11 +644,11 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
644
644
  | `a28aa368453f` | certain | It shipped and it is live. Say what you see — your review coaches the grader and never blocks what shipped. | `modules/hall-ui/public/studio.js:236` |
645
645
  | `d751fe51e178` | certain | It shipped and it is live. Your review coaches the grader on how it judged the work, then remakes or tweaks th | `modules/hall-ui/public/studio.js:237` |
646
646
  | `7abb3b104824` | certain | It should read | `modules/hall-ui/public/market-watch-lib.js:187` |
647
- | `a21788f84e29` | certain | Its author gave no scope recommendation. | `modules/hall-ui/public/board-room.js:311` |
647
+ | `a21788f84e29` | certain | Its author gave no scope recommendation. | `modules/hall-ui/public/board-room.js:312` |
648
648
  | `a21788f84e29` | certain | Its author gave no scope recommendation. | `modules/hall-ui/public/idea.js:162` |
649
649
  | `49dc6885e5ff` | certain | Its author has been asked to set a cash price. Credits cannot buy a module. | `modules/hall-ui/public/modules.js:348` |
650
650
  | `b0bcb3f844ec` | certain | Its author marked this as needing development — anyone can turn it into a Full Idea, and they keep the credit | `modules/hall-ui/public/idea.js:473` |
651
- | `680491549e47` | certain | Its author recommends this becomes | `modules/hall-ui/public/board-room.js:310` |
651
+ | `680491549e47` | certain | Its author recommends this becomes | `modules/hall-ui/public/board-room.js:311` |
652
652
  | `680491549e47` | certain | Its author recommends this becomes | `modules/hall-ui/public/idea.js:166` |
653
653
  | `a4458b500a21` | certain | Its name | `modules/hall-ui/public/market-watch-lib.js:163` |
654
654
  | `d42a078838bf` | certain | Its notes could not be read. | `modules/hall-ui/public/settings-software-update.js:113` |
@@ -1180,7 +1180,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1180
1180
  | `3a4e95274918` | certain | Send for approval | `modules/hall-ui/public/paintbrush.js:169` |
1181
1181
  | `53cc6d8dd23c` | certain | Send invitation | `modules/hall-ui/public/goals-render.js:1238` |
1182
1182
  | `ce7697117843` | certain | Send it back | `modules/hall-ui/public/approval-queue.js:187` |
1183
- | `2ffe1229b73b` | certain | Send objection | `modules/hall-ui/public/board-room.js:391` |
1183
+ | `2ffe1229b73b` | certain | Send objection | `modules/hall-ui/public/board-room.js:392` |
1184
1184
  | `a39b0f9d86e5` | certain | Serving {…} — read from the box itself, {…}. | `modules/hall-ui/public/deploy.js:619` |
1185
1185
  | `d14e8e9acaff` | certain | Session ended. | `modules/hall-ui/public/settings-sessions.js:85` |
1186
1186
  | `55e0a34bafed` | certain | Session log{…} | `modules/hall-ui/public/task.js:495` |
@@ -1321,7 +1321,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1321
1321
  | `b12e1438a8e8` | certain | That review could not be opened. It may have been removed, or you may not have access. | `modules/hall-ui/public/studio.js:1203` |
1322
1322
  | `da217056b514` | certain | That’s you | `modules/hall-ui/public/roster.js:203` |
1323
1323
  | `788f267e4e39` | certain | The Autobongos controls are for the {…}. | `modules/hall-ui/public/gate.js:74` |
1324
- | `c5e6bdd32fe3` | certain | The Board Room is open to the builders this project’s constitution seats on its board. | `modules/hall-ui/public/board-room.js:563` |
1324
+ | `c5e6bdd32fe3` | certain | The Board Room is open to the builders this project’s constitution seats on its board. | `modules/hall-ui/public/board-room.js:564` |
1325
1325
  | `6e93ef49d63f` | certain | The adversarial panel. Each worker reports pass, concerns or fail on its own; the gating workers must all pass | `modules/hall-ui/public/task.js:439` |
1326
1326
  | `89c7bd971440` | certain | The approved pages could not be read just now. | `modules/hall-ui/public/profile.js:782` |
1327
1327
  | `600b90074021` | certain | The art pipeline draws tiles and sprites with Google AI Studio (Gemini). Needed only if you generate art — you | `modules/hall-ui/public/settings.html:245` |
@@ -1606,7 +1606,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1606
1606
  | `ef4614d549d5` | certain | Why retire this task? | `modules/hall-ui/public/task.js:840` |
1607
1607
  | `2b047710fd6d` | certain | Why this goal is being retired (required) | `modules/hall-ui/public/goals-render.js:1039` |
1608
1608
  | `3134750a98fe` | certain | Why you'd like in | `modules/hall-ui/public/hall-render.js:236` |
1609
- | `9012c7526e31` | certain | Why — the author acts on this | `modules/hall-ui/public/board-room.js:391` |
1609
+ | `9012c7526e31` | certain | Why — the author acts on this | `modules/hall-ui/public/board-room.js:392` |
1610
1610
  | `38f310046053` | certain | Why, in one line | `modules/hall-ui/public/market-watch-lib.js:190` |
1611
1611
  | `a3ac116b499a` | certain | Why? | `modules/hall-ui/public/copy-desk.html:99` |
1612
1612
  | `c5fd76a9e03d` | certain | Won {…} | `modules/hall-ui/public/profile.js:201` |
@@ -1638,7 +1638,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1638
1638
  | `da274462bb30` | certain | You have no open asks. | `modules/hall-ui/public/collab.html:83` |
1639
1639
  | `6b759151f722` | certain | You have reached the end. The button below is now active. | `modules/hall-ui/public/primer.js:384` |
1640
1640
  | `044387a6f90c` | certain | You haven’t joined any open goals. | `modules/hall-ui/public/goals.js:100` |
1641
- | `819dfb951611` | certain | You {…}. | `modules/hall-ui/public/board-room.js:475` |
1641
+ | `819dfb951611` | certain | You {…}. | `modules/hall-ui/public/board-room.js:476` |
1642
1642
  | `8e1915b19274` | certain | You're in the {…} server with your rank role. | `modules/hall-ui/public/settings.js:530` |
1643
1643
  | `56cfdcc60079` | certain | You're not signed in yet. Sign in to manage your connections, your keys and the way the hall behaves. | `modules/hall-ui/public/settings.html:44` |
1644
1644
  | `56efb277e77f` | certain | Your AI balance | `modules/hall-ui/public/ai-studio.js:582` |
@@ -1825,7 +1825,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1825
1825
  | `e2c643fbf4d6` | certain | npm install | `modules/hall-ui/public/setup-gate.js:34` |
1826
1826
  | `f3733d6a021e` | certain | npx bongos setup | `modules/hall-ui/public/setup-gate.js:35` |
1827
1827
  | `77227890fb2e` | certain | number = names watched | `modules/hall-ui/public/market-watch-settings.js:127` |
1828
- | `e90b5601ce16` | certain | objected to | `modules/hall-ui/public/board-room.js:184` |
1828
+ | `e90b5601ce16` | certain | objected to | `modules/hall-ui/public/board-room.js:185` |
1829
1829
  | `09172f45e6da` | certain | of the 90% needed | `modules/hall-ui/public/primer.html:96` |
1830
1830
  | `ee8625a0b2de` | certain | of {…} · {…} | `modules/hall-ui/public/market-watch-settings.js:85` |
1831
1831
  | `973f396ac861` | certain | one line | `modules/hall-ui/public/market-watch-rules-lib.js:137` |
@@ -1995,7 +1995,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1995
1995
  | `dd7f7d6a97fe` | certain | your share this month | `modules/hall-ui/public/street.html:91` |
1996
1996
  | `c612ab426c2f` | certain | your sky | `modules/hall-ui/public/thinking.html:72` |
1997
1997
  | `3491d859e66a` | certain | your thinking | `modules/hall-ui/public/thinking.html:77` |
1998
- | `66d979d0db89` | certain | your vote | `modules/hall-ui/public/board-room.js:179` |
1998
+ | `66d979d0db89` | certain | your vote | `modules/hall-ui/public/board-room.js:180` |
1999
1999
  | `78a3ca07861a` | certain | { const parts = window.OTB.splitCriterion ? window.OTB.splitCriterion(c.criterion_md) : { label: c.criterion_m | `modules/hall-ui/public/goals-render.js:715` |
2000
2000
  | `6796a81b7266` | certain | { if (row.key === 'ship_mode' && row.editable) return rowHtml(row, shipBodyHtml(row)); if (row.key === 'tweak_ | `modules/hall-ui/public/project-settings.js:245` |
2001
2001
  | `e4b36621d4f1` | certain | {…} Nothing needs doing — this page shows the result when it lands. | `modules/hall-ui/public/deploy.js:222` |
@@ -2042,7 +2042,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
2042
2042
  | `23872e0af7a4` | certain | {…} · Optional. Without one, generations run on ours and are charged to your balance. | `modules/hall-ui/public/settings-ai-keys.js:45` |
2043
2043
  | `b3757ba3bb46` | certain | {…} · Stored encrypted; never shown again after saving. | `modules/hall-ui/public/settings.js:731` |
2044
2044
  | `a6e4ed98fbea` | certain | {…} · revoke it any time under Active sessions. | `modules/hall-ui/public/settings.js:637` |
2045
- | `d73256fbc395` | certain | {…} · who votes, and how it {…} | `modules/hall-ui/public/board-room.js:484` |
2045
+ | `d73256fbc395` | certain | {…} · who votes, and how it {…} | `modules/hall-ui/public/board-room.js:485` |
2046
2046
  | `1ac723849449` | certain | {…} · {…}{…} | `modules/hall-ui/public/notes-panel.js:39` |
2047
2047
  | `1ac723849449` | certain | {…} · {…}{…} | `modules/hall-ui/public/sky-interaction.js:33` |
2048
2048
  | `ddd2bfc1fb60` | certain | {…} — how to use it | `modules/hall-ui/public/modules.js:662` |
@@ -1721,7 +1721,7 @@
1721
1721
  "text": "Its author gave no scope recommendation.",
1722
1722
  "occurrences": 2,
1723
1723
  "at": [
1724
- "modules/hall-ui/public/board-room.js:311",
1724
+ "modules/hall-ui/public/board-room.js:312",
1725
1725
  "modules/hall-ui/public/idea.js:162"
1726
1726
  ]
1727
1727
  },
@@ -1731,7 +1731,7 @@
1731
1731
  "text": "Its author recommends this becomes",
1732
1732
  "occurrences": 2,
1733
1733
  "at": [
1734
- "modules/hall-ui/public/board-room.js:310",
1734
+ "modules/hall-ui/public/board-room.js:311",
1735
1735
  "modules/hall-ui/public/idea.js:166"
1736
1736
  ]
1737
1737
  },
@@ -3179,7 +3179,7 @@
3179
3179
  "surface": "builders-hall",
3180
3180
  "text": ", // membership",
3181
3181
  "file": "modules/hall-ui/public/board-room.js",
3182
- "line": 316,
3182
+ "line": 317,
3183
3183
  "origin": "js-markup",
3184
3184
  "confidence": "certain"
3185
3185
  },
@@ -6167,7 +6167,7 @@
6167
6167
  "surface": "builders-hall",
6168
6168
  "text": "Could not vote: {…}",
6169
6169
  "file": "modules/hall-ui/public/board-room.js",
6170
- "line": 427,
6170
+ "line": 428,
6171
6171
  "origin": "js-notice",
6172
6172
  "confidence": "certain"
6173
6173
  },
@@ -8039,7 +8039,7 @@
8039
8039
  "surface": "builders-hall",
8040
8040
  "text": "It cleared the completeness grade at {…}/100.",
8041
8041
  "file": "modules/hall-ui/public/board-room.js",
8042
- "line": 342,
8042
+ "line": 343,
8043
8043
  "origin": "js-markup",
8044
8044
  "confidence": "certain"
8045
8045
  },
@@ -8120,7 +8120,7 @@
8120
8120
  "surface": "builders-hall",
8121
8121
  "text": "Its author gave no scope recommendation.",
8122
8122
  "file": "modules/hall-ui/public/board-room.js",
8123
- "line": 311,
8123
+ "line": 312,
8124
8124
  "origin": "js-markup",
8125
8125
  "confidence": "certain"
8126
8126
  },
@@ -8156,7 +8156,7 @@
8156
8156
  "surface": "builders-hall",
8157
8157
  "text": "Its author recommends this becomes",
8158
8158
  "file": "modules/hall-ui/public/board-room.js",
8159
- "line": 310,
8159
+ "line": 311,
8160
8160
  "origin": "js-markup",
8161
8161
  "confidence": "certain"
8162
8162
  },
@@ -12944,7 +12944,7 @@
12944
12944
  "surface": "builders-hall",
12945
12945
  "text": "Send objection",
12946
12946
  "file": "modules/hall-ui/public/board-room.js",
12947
- "line": 391,
12947
+ "line": 392,
12948
12948
  "origin": "js-markup",
12949
12949
  "confidence": "certain"
12950
12950
  },
@@ -14213,7 +14213,7 @@
14213
14213
  "surface": "builders-hall",
14214
14214
  "text": "The Board Room is open to the builders this project’s constitution seats on its board.",
14215
14215
  "file": "modules/hall-ui/public/board-room.js",
14216
- "line": 563,
14216
+ "line": 564,
14217
14217
  "origin": "js-text-assign",
14218
14218
  "confidence": "certain"
14219
14219
  },
@@ -16778,7 +16778,7 @@
16778
16778
  "surface": "builders-hall",
16779
16779
  "text": "Why — the author acts on this",
16780
16780
  "file": "modules/hall-ui/public/board-room.js",
16781
- "line": 391,
16781
+ "line": 392,
16782
16782
  "origin": "js-markup",
16783
16783
  "confidence": "certain"
16784
16784
  },
@@ -17066,7 +17066,7 @@
17066
17066
  "surface": "builders-hall",
17067
17067
  "text": "You {…}.",
17068
17068
  "file": "modules/hall-ui/public/board-room.js",
17069
- "line": 475,
17069
+ "line": 476,
17070
17070
  "origin": "js-markup",
17071
17071
  "confidence": "certain"
17072
17072
  },
@@ -18749,7 +18749,7 @@
18749
18749
  "surface": "builders-hall",
18750
18750
  "text": "objected to",
18751
18751
  "file": "modules/hall-ui/public/board-room.js",
18752
- "line": 184,
18752
+ "line": 185,
18753
18753
  "origin": "js-markup",
18754
18754
  "confidence": "certain"
18755
18755
  },
@@ -20279,7 +20279,7 @@
20279
20279
  "surface": "builders-hall",
20280
20280
  "text": "your vote",
20281
20281
  "file": "modules/hall-ui/public/board-room.js",
20282
- "line": 179,
20282
+ "line": 180,
20283
20283
  "origin": "js-markup",
20284
20284
  "confidence": "certain"
20285
20285
  },
@@ -20702,7 +20702,7 @@
20702
20702
  "surface": "builders-hall",
20703
20703
  "text": "{…} · who votes, and how it {…}",
20704
20704
  "file": "modules/hall-ui/public/board-room.js",
20705
- "line": 484,
20705
+ "line": 485,
20706
20706
  "origin": "js-markup",
20707
20707
  "confidence": "certain"
20708
20708
  },
@@ -3400,5 +3400,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3400
3400
  landed since 1.24.72 with no explicit bump. run 42712461933. (task 1002620)
3401
3401
  1.24.74 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3402
3402
  landed since 1.24.73 with no explicit bump. run 42715090111. (task 1002620)
3403
+ 1.24.75 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3404
+ landed since 1.24.74 with no explicit bump. run 42722523085. (task 1002620)
3403
3405
  ---------------------------------------------------------------------------
3404
3406
  ```
@@ -215,7 +215,7 @@
215
215
  "surface": "builders",
216
216
  "title": "Board Room",
217
217
  "states_read": ["room","out","sitting-meta","objection-form"],
218
- "files_hash": "9461cd6cbc270ae2",
218
+ "files_hash": "0e6e17f03283a4a6",
219
219
  "reading_hash": "020553cb2a438d56",
220
220
  "counts": {"lines":51,"placed":18,"shared":9,"unplaced":24},
221
221
  "omitted": {"data":15},
@@ -238,7 +238,7 @@
238
238
  {"key":"L0015","section":"{…} Full Idea","text":"It counts toward passing","placement":"unplaced"},
239
239
  {"key":"L0016","section":"{…} Full Idea","text":"Object","placement":"unplaced"},
240
240
  {"key":"L0017","section":"{…} Full Idea","text":"It goes back with your reason","placement":"unplaced"},
241
- {"key":"L0018","section":"{…} Full Idea","text":"Its author recommends this becomes","placement":"placed","file":"modules/hall-ui/public/board-room.js","line":310,"string_id":"680491549e47"},
241
+ {"key":"L0018","section":"{…} Full Idea","text":"Its author recommends this becomes","placement":"placed","file":"modules/hall-ui/public/board-room.js","line":311,"string_id":"680491549e47"},
242
242
  {"key":"L0019","section":"{…} Full Idea","text":"{…} of {…}","placement":"placed","file":"modules/hall-ui/public/sky-panel.js","line":120,"string_id":"73c1372b43d9","occurrences":2},
243
243
  {"key":"L0020","section":"{…} Full Idea","text":". The {…} decides.","placement":"unplaced"},
244
244
  {"key":"L0021","section":"{…} Full Idea","text":"What made you have the idea","placement":"unplaced"},
@@ -246,18 +246,18 @@
246
246
  {"key":"L0023","section":"{…} Full Idea","text":"Scope it as best as you can here","placement":"unplaced"},
247
247
  {"key":"L0024","section":"{…} Full Idea","text":"What should an {…} or {…} know prior to building","placement":"unplaced"},
248
248
  {"key":"L0025","section":"{…} Full Idea","text":"How is this long term justified in the project","placement":"unplaced"},
249
- {"key":"L0026","section":"{…} Full Idea","text":"{…} · who votes, and how it {…}","placement":"placed","file":"modules/hall-ui/public/board-room.js","line":480,"string_id":"d73256fbc395","occurrences":9},
249
+ {"key":"L0026","section":"{…} Full Idea","text":"{…} · who votes, and how it {…}","placement":"placed","file":"modules/hall-ui/public/board-room.js","line":485,"string_id":"d73256fbc395","occurrences":9},
250
250
  {"key":"L0027","section":"Constitutional amendment amendment","text":"Constitutional amendment","placement":"unplaced","occurrences":3},
251
251
  {"key":"L0028","section":"Constitutional amendment amendment","text":"amendment","placement":"unplaced","occurrences":3},
252
252
  {"key":"L0029","section":"Constitutional amendment amendment","text":"open","placement":"placed","file":"modules/hall-ui/public/approval-queue.js","line":173,"string_id":"9a803563c948"},
253
253
  {"key":"L0030","section":"Constitutional amendment amendment","text":"no clock — waits for a {…} yes","placement":"unplaced"},
254
254
  {"key":"L0031","section":"Constitutional amendment amendment","text":"Proposes","placement":"unplaced","occurrences":3},
255
- {"key":"L0032","section":"Constitutional amendment amendment","text":"{…} · who votes, and how it {…}","placement":"placed","file":"modules/hall-ui/public/board-room.js","line":480,"string_id":"d73256fbc395","occurrences":3},
255
+ {"key":"L0032","section":"Constitutional amendment amendment","text":"{…} · who votes, and how it {…}","placement":"placed","file":"modules/hall-ui/public/board-room.js","line":485,"string_id":"d73256fbc395","occurrences":3},
256
256
  {"key":"L0033","section":"Constitutional amendment amendment","text":"Closed sittings","placement":"unplaced"},
257
257
  {"key":"L0034","section":"{…} Full Idea","text":"passed","placement":"placed","file":"modules/hall-ui/public/government.js","line":679,"string_id":"da5368d10f85","occurrences":8},
258
258
  {"key":"L0035","section":"{…} and {…}","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232","occurrences":5},
259
259
  {"key":"L0036","section":"{…} and {…}","text":"Full Idea","placement":"unplaced","occurrences":5},
260
- {"key":"L0037","section":"{…} and {…}","text":"{…} · who votes, and how it {…}","placement":"placed","file":"modules/hall-ui/public/board-room.js","line":480,"string_id":"d73256fbc395","occurrences":5},
260
+ {"key":"L0037","section":"{…} and {…}","text":"{…} · who votes, and how it {…}","placement":"placed","file":"modules/hall-ui/public/board-room.js","line":485,"string_id":"d73256fbc395","occurrences":5},
261
261
  {"key":"L0038","section":"Constitutional amendment amendment","text":"passed","placement":"placed","file":"modules/hall-ui/public/government.js","line":679,"string_id":"da5368d10f85","occurrences":2},
262
262
  {"key":"L0039","section":"Constitutional amendment amendment","text":"{\"{…}_{…}\":\"{…}\",\"membership\":\"{…}\",\"window_minutes\":1440,\"close_early_on_{…}_turnout\":true}","placement":"unplaced","occurrences":2},
263
263
  {"key":"L0040","section":"Constitutional amendment amendment","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232","occurrences":2},
@@ -974,8 +974,8 @@
974
974
  "surface": "builders",
975
975
  "title": "Builders' Hall",
976
976
  "states_read": ["home","out","founding","genesis","decide","demo"],
977
- "files_hash": "5c8e267006cb62e1",
978
- "reading_hash": "4cc2e5e86483a66e",
977
+ "files_hash": "0680b2faae3f956f",
978
+ "reading_hash": "ae7a0c466a3aee85",
979
979
  "counts": {"lines":28,"placed":9,"shared":10,"unplaced":9},
980
980
  "omitted": {"data":4},
981
981
  "notes": ["founding: an action failed (page.focus: Timeout 6000ms exceeded.); read as it stood"],
@@ -999,7 +999,7 @@
999
999
  {"key":"L0017","section":"Needs your attention13","text":"Ideas awaiting triage","placement":"placed","file":"modules/hall-ui/public/builders.js","line":320,"string_id":"e181637c5a5e"},
1000
1000
  {"key":"L0018","section":"Needs your attention13","text":"Requires project {…} fix","placement":"unplaced"},
1001
1001
  {"key":"L0019","section":"footer","text":"Sign out","placement":"shared","file":"modules/hall-ui/public/shell.js","line":533,"string_id":"1883ad7f07fc"},
1002
- {"key":"L0020","section":"Needs your attention13","text":"core 1.24.71","placement":"unplaced"},
1002
+ {"key":"L0020","section":"Needs your attention13","text":"core 1.24.74","placement":"unplaced"},
1003
1003
  {"key":"L0021","section":"Needs your attention13","text":"Skip","placement":"unplaced"},
1004
1004
  {"key":"L0022","section":"Needs your attention13","text":"Built on","placement":"placed","file":"modules/hall-ui/public/welcome-boot.js","line":181,"string_id":"fef59346ffee"},
1005
1005
  {"key":"L0023","section":"Needs your attention13","text":"cloudbongos.com","placement":"unplaced"},
@@ -255,18 +255,21 @@ function validateVote({ direction, reasonMd, sectionKey } = {}) {
255
255
  // { ok: true, vote, fresh: true, karma_delta, self_cast } — recorded now
256
256
  // { ok: true, vote, fresh: false, self_cast } — idempotent re-cast
257
257
  // { ok: false, reason: 'item_not_found' | 'item_closed' | 'vote_window_expired'
258
- // | 'not_board_member' | <validateVote reason> }
258
+ // | 'not_board_member' | 'bad_cast_via' | <validateVote reason> }
259
+ // `castVia` (task 1005121, government_038) is internal — never read from a request: 'ballot', or 'stage_close' (genesis-close.js: a founder's confirm given by closing a genesis stage — genesis_stage + yes only, no vote karma).
259
260
  // 'anti_self_vote' is GONE as of task 1003273 — an author may cast; what they
260
261
  // cannot do is decide alone, which shouldCloseAfterVote and tallyOutcome enforce.
261
262
  // A self-cast is recorded and visible but earns no karma (karma_delta: 0).
262
- async function castVote({ itemId, voterId, direction, reasonMd = null, sectionKey = null } = {}) {
263
+ async function castVote({ itemId, voterId, direction, reasonMd = null, sectionKey = null, castVia = 'ballot' } = {}) {
263
264
  const id = Number(itemId);
264
265
  if (!Number.isFinite(id) || id <= 0) return { ok: false, reason: 'item_not_found' };
266
+ if (castVia !== 'ballot' && castVia !== 'stage_close') return { ok: false, reason: 'bad_cast_via' };
265
267
  const v = validateVote({ direction, reasonMd, sectionKey });
266
268
  if (!v.ok) return v;
267
269
 
268
270
  const item = await db.getBoardItemForVote(id);
269
271
  if (!item) return { ok: false, reason: 'item_not_found' };
272
+ if (castVia === 'stage_close' && (item.subject_type !== 'genesis_stage' || v.direction !== 'yes')) return { ok: false, reason: 'bad_cast_via' };
270
273
  if (item.closed_at) return { ok: false, reason: 'item_closed' };
271
274
  // The clock is checked here as well as by the R11 poller: a vote landing in
272
275
  // the gap between expiry and the sweep would be a vote after time. The
@@ -309,6 +312,7 @@ async function castVote({ itemId, voterId, direction, reasonMd = null, sectionKe
309
312
 
310
313
  const inserted = await db.insertBoardVote({
311
314
  itemId: id, voterId, direction: v.direction, reasonMd: v.reasonMd, sectionKey: v.sectionKey,
315
+ ...(castVia === 'ballot' ? {} : { castVia }), // db defaults a ballot
312
316
  });
313
317
  if (!inserted) {
314
318
  // UNIQUE(item_id, voter_id) — the idempotent path. Return what stands.
@@ -317,8 +321,8 @@ async function castVote({ itemId, voterId, direction, reasonMd = null, sectionKe
317
321
  }
318
322
  // Karma rides the FRESH insert only, and the whole route refused a closed or
319
323
  // expired item above, so every paid vote is by construction in-window.
320
- if (isSelfCast) {
321
- return { ok: true, vote: inserted, fresh: true, karma_delta: 0, self_cast: true };
324
+ if (isSelfCast || castVia === 'stage_close') {
325
+ return { ok: true, vote: inserted, fresh: true, karma_delta: 0, self_cast: isSelfCast };
322
326
  }
323
327
  await db.insertVoteKarma({ voterId, itemId: id, delta: VOTE_KARMA_DELTA });
324
328
  return { ok: true, vote: inserted, fresh: true, karma_delta: VOTE_KARMA_DELTA, self_cast: false };
@@ -1103,6 +1107,7 @@ async function genesisStageStatus() {
1103
1107
  remaining_seconds: item.remaining_seconds ?? null,
1104
1108
  voted: cast.map((v) => ({
1105
1109
  builder_id: String(v.voter_id), github_login: v.github_login, display_name: v.display_name, direction: v.direction,
1110
+ cast_via: v.cast_via || 'ballot',
1106
1111
  })),
1107
1112
  waiting_on: members.filter((m) => !castBy.has(String(m.builder_id))).map((m) => ({
1108
1113
  builder_id: m.builder_id, github_login: m.github_login, display_name: m.display_name,
@@ -1155,6 +1160,7 @@ async function boardRoomView({ closedLimit = 20, viewerId = null } = {}) {
1155
1160
  reason_md: v.reason_md,
1156
1161
  section_key: v.section_key,
1157
1162
  cast_at: v.cast_at,
1163
+ cast_via: v.cast_via || 'ballot', // task 1005121: a ballot, or a confirm given by closing a genesis stage
1158
1164
  });
1159
1165
  }
1160
1166
  const project = (item) => {
@@ -494,13 +494,15 @@ async function getBoardItemAuthor(item) {
494
494
  // The open ballot's one write. Idempotency is STRUCTURAL — UNIQUE(item_id,
495
495
  // voter_id); the loser of a double-cast gets null and the caller reads the
496
496
  // standing vote back.
497
- async function insertBoardVote({ itemId, voterId, direction, reasonMd = null, sectionKey = null }) {
497
+ // cast_via (government_038, task 1005121): 'ballot', or 'stage_close' for the
498
+ // confirm a founder gives by closing a genesis stage. board.js decides which.
499
+ async function insertBoardVote({ itemId, voterId, direction, reasonMd = null, sectionKey = null, castVia = 'ballot' }) {
498
500
  const { rows } = await pool.query(
499
- `INSERT INTO government_board_votes (item_id, voter_id, direction, reason_md, section_key)
500
- VALUES ($1, $2, $3, $4, $5)
501
+ `INSERT INTO government_board_votes (item_id, voter_id, direction, reason_md, section_key, cast_via)
502
+ VALUES ($1, $2, $3, $4, $5, $6)
501
503
  ON CONFLICT (item_id, voter_id) DO NOTHING
502
504
  RETURNING *`,
503
- [itemId, voterId, direction, reasonMd, sectionKey],
505
+ [itemId, voterId, direction, reasonMd, sectionKey, castVia],
504
506
  );
505
507
  return rows[0] || null;
506
508
  }
@@ -1321,7 +1323,7 @@ async function insertVoteKarma({ voterId, itemId, delta }) {
1321
1323
  }
1322
1324
 
1323
1325
  // ── the constitution in force (task 1005140) ─────────────────────────────────
1324
- // ONE row (government_037). The board block is stored exactly as sanitizeBoard
1326
+ // ONE row (government_038). The board block is stored exactly as sanitizeBoard
1325
1327
  // returns it; constitution.js owns the rules for when it is written and how the
1326
1328
  // running process is brought in step with it. `q` is a client or the pool.
1327
1329
  async function getConstitutionRow(q = pool) {
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+ // modules/government/genesis-close.js — closing a genesis stage is ONE call
3
+ // (task 1005121). Its own file so board.js stays under the size ratchet; it is
4
+ // composed only from board.js's exported functions (looked up at call time, so a
5
+ // test's stubs on board's db bite exactly as they do for board itself).
6
+ const board = require('./board.js');
7
+
8
+ // Close a genesis stage in ONE call (task 1005121; owner ruling 2026-11-07:
9
+ // "closing a stage shouldn't be such a big thing"). Opens the sitting (or reads
10
+ // the one already open) and, where the item's constitution says one person
11
+ // decides (first_ratifier) and the caller is a member under its snapshot, casts
12
+ // the caller's yes in the same breath — recorded cast_via 'stage_close', so the
13
+ // ledger tells a confirm given by closing the stage from a clicked ballot — and
14
+ // closes it. The same rule at every stage: nothing here forks on which stage.
15
+ // Returns, never throws for a refusal:
16
+ // { closed: true, item, stage, cast } — the sitting passed
17
+ // { closed: false, item, stage, opened, status } — a real sitting waits;
18
+ // `status` is genesisStageStatus' entry for this stage (who has voted, who
19
+ // it waits on, the rule in words, the clock)
20
+ // { closed: false, reason, unfinished? } — openGenesisStageClose refused
21
+ async function closeGenesisStage({ stageKey, taskIds, closedBy = null } = {}) {
22
+ const r = await board.openGenesisStageClose({ stageKey, taskIds, openedBy: closedBy });
23
+ if (!r.opened && r.reason !== 'already_open') return { closed: false, reason: r.reason, unfinished: r.unfinished || [] };
24
+ const item = r.item;
25
+ const stage = r.stage || board.GENESIS_STAGES.find((s) => s.key === stageKey);
26
+ if (!item) return { closed: false, reason: 'already_open' };
27
+ const constitution = item.constitution || {};
28
+ let cast = null;
29
+ if (constitution.pass_rule === 'first_ratifier' && closedBy != null && closedBy !== '') {
30
+ const v = await board.castVote({ itemId: item.id, voterId: closedBy, direction: 'yes', castVia: 'stage_close' });
31
+ if (v.ok) {
32
+ cast = { cast_via: v.vote ? v.vote.cast_via || 'stage_close' : 'stage_close', fresh: v.fresh };
33
+ const c = await board.maybeCloseAfterVote(item.id);
34
+ if (c && c.closed && c.outcome === 'passed') return { closed: true, item: c.item || item, stage, cast };
35
+ }
36
+ }
37
+ let status = null;
38
+ try {
39
+ const all = await board.genesisStageStatus();
40
+ status = (all.stages || []).find((s) => s.key === stage.key) || null;
41
+ } catch (_) { /* the answer still says a sitting is open, and its id */ }
42
+ return { closed: false, item, stage, opened: !!r.opened, status, cast };
43
+ }
44
+
45
+ module.exports = { closeGenesisStage };
@@ -0,0 +1,23 @@
1
+ -- government_038_vote_cast_via.sql — HOW a vote was cast (task 1005121).
2
+ --
3
+ -- Closing a genesis stage is one call: where the constitution says one person
4
+ -- decides (first_ratifier) and that person is the founder asking to close it, the
5
+ -- close casts their confirming yes in the same breath (board.js closeGenesisStage).
6
+ -- The owner's condition (2026-11-07): a ratification carries a person's name, so
7
+ -- the record must say HOW it was cast — a confirm given by closing the stage — not
8
+ -- merely that the founder voted. Every vote before this column is a ballot.
9
+ --
10
+ -- cast_via 'ballot' a vote cast in the Board Room (the vote route)
11
+ -- 'stage_close' the confirm given by closing a genesis stage; only
12
+ -- board.js writes it, only on a genesis_stage item, only yes
13
+
14
+ BEGIN;
15
+
16
+ ALTER TABLE government_board_votes
17
+ ADD COLUMN IF NOT EXISTS cast_via text NOT NULL DEFAULT 'ballot';
18
+
19
+ ALTER TABLE government_board_votes DROP CONSTRAINT IF EXISTS government_board_votes_cast_via_check;
20
+ ALTER TABLE government_board_votes ADD CONSTRAINT government_board_votes_cast_via_check
21
+ CHECK (cast_via IN ('ballot', 'stage_close') AND (cast_via = 'ballot' OR direction = 'yes'));
22
+
23
+ COMMIT;
@@ -126,12 +126,11 @@ const governmentPort = {
126
126
  // outcome is written by a close, never by the module reading it.
127
127
  boardPassForIdea: board.boardPassForIdea,
128
128
  boardPassedIdeaIds: board.boardPassedIdeaIds,
129
- // task 1004429 (BV2.PS04): a finished genesis stage is put to the board — the
130
- // genesis home (task 1004423) calls this when a stage's tasks are done. The
131
- // finished-tasks gate lives inside board.js; the item is an ordinary one under
132
- // the constitution in force. And the founding band's read: per stage, what it
133
- // is waiting on. A pass is announced as `board.genesis_stage.passed`.
129
+ // task 1004429 (BV2.PS04): a finished genesis stage is put to the board (the gate
130
+ // lives in board.js; an ordinary item under the constitution in force), and the
131
+ // founding band's read. task 1005121: closeGenesisStage closes it in one call.
134
132
  openGenesisStageClose: board.openGenesisStageClose,
133
+ closeGenesisStage: require('../genesis-close').closeGenesisStage, // task 1005121: one call closes a stage
135
134
  // task 1004941: POST /government/board/matters' own openMatter, for a module's proposal (callers gate on board.item.open)
136
135
  openMatter: board.openMatter,
137
136
  // task 1004427 (BV2.PS13): the genesis charter + legislation choice, filed as
@@ -100,6 +100,7 @@
100
100
  // rank/permission-CRUD refusals stayed with the Permissions surface — this page
101
101
  // cannot provoke them.
102
102
  const ERROR_COPY = {
103
+ bad_cast_via: 'That vote could not be recorded that way — cast it from the ballot.',
103
104
  not_board_member: 'Only members of this item’s board may vote on it — membership follows the rules that were in force when the window opened.',
104
105
  item_closed: 'This sitting has closed — refreshing.',
105
106
  vote_window_expired: 'This sitting’s clock has run out — the sweep is closing it.',
@@ -334,19 +334,14 @@
334
334
  return window.OTB.sleep(3500);
335
335
  }).then(function () { window.location.reload(); }));
336
336
  } else if (t.hasAttribute('data-genesis-close')) {
337
- // The solo fast-track (spec D4): when the founder's one yes decides the sitting
338
- // and nobody else is asked, the close and the confirm are one click.
337
+ // One call (task 1005121): where the founder's one yes decides, the server
338
+ // closes the stage with it; otherwise it answers 202 with what the sitting
339
+ // waits on, and that sentence is shown.
339
340
  act(send('POST', '/founding/stages/' + encodeURIComponent(t.getAttribute('data-genesis-close')) + '/close', {})
340
- .then(function () { return send('GET', '/founding/genesis'); })
341
- .then(function (d) {
342
- var g = d && d.genesis;
343
- var s = g && g.stages.find(function (x) { return x.state === 'now'; });
344
- var b = s && s.board;
345
- var solo = b && b.state === 'sitting' && b.one_vote_decides && b.item_id
346
- && (b.waiting_on || []).length === 1 && String(b.waiting_on[0].builder_id) === String(d.viewer_id);
347
- return solo ? send('POST', '/government/board/items/' + encodeURIComponent(b.item_id) + '/votes', { direction: 'yes' }) : null;
348
- })
349
- .then(function () { return load(host, state); }));
341
+ .then(function (out) {
342
+ if (out && out.closed === false && out.next) toast(out.next);
343
+ return load(host, state);
344
+ }));
350
345
  } else if (t.hasAttribute('data-genesis-confirm')) {
351
346
  act(send('POST', '/government/board/items/' + encodeURIComponent(t.getAttribute('data-genesis-confirm')) + '/votes', { direction: 'yes' })
352
347
  .then(function () { return load(host, state); }));
@@ -326,10 +326,25 @@ async function applyGovernmentChoice(form, builderId, s) {
326
326
  return { state: 'not_filed', form, reason };
327
327
  }
328
328
 
329
- // closeStage — put the current stage to the Board Room. The finished-tasks gate is the
330
- // government module's (it checks the ids against the board); this only names the
331
- // stage's tasks. With no government module there is no Board Room to put it to, and
332
- // the founder's confirm closes the stage itself once its tasks are finished.
329
+ // closeStage — CLOSE the current stage, in one call (task 1005121; owner ruling
330
+ // 2026-11-07: "closing a stage shouldn't be such a big thing"). The answer says, by
331
+ // itself, which of two things happened — a caller never needs a second request:
332
+ //
333
+ // { ok: true, closed: true, stage, next_stage, item_id, confirm }
334
+ // The stage is closed and the project has moved on (`next_stage` null at
335
+ // legislation: the alive door, POST /founding/alive, is open instead).
336
+ // `confirm` is 'stage_close' when the founder's own yes carried it — the
337
+ // government port records that HOW on the vote (cast_via).
338
+ // { ok: false, closed: false, reason: 'sitting_open', stage, item_id, already_open,
339
+ // waiting: { pass_rule, needs, voted, waiting_on, closes_at, remaining_seconds },
340
+ // next }
341
+ // A real Board Room sitting is deciding it (more than one person, or a rule
342
+ // one yes cannot carry). Never ok: true — the stage did NOT close. The route
343
+ // answers 202 with this whole body; `next` says what to do, in words.
344
+ //
345
+ // With a government module the port's closeGenesisStage does the deciding (its
346
+ // finished-tasks gate, the founder's confirm where one person decides). With none,
347
+ // the founder's ask closes a finished stage itself. Same rule at every stage.
333
348
  async function closeStage(stageKey, builderId, deps = {}) {
334
349
  const s = seams(deps);
335
350
  const mode = await reconcile(s);
@@ -337,17 +352,59 @@ async function closeStage(stageKey, builderId, deps = {}) {
337
352
  if (mode.stage !== stageKey) return { ok: false, reason: 'not_current_stage', stage: mode.stage };
338
353
  const all = s.lifecycle && typeof s.lifecycle.genesisTasks === 'function' ? await s.lifecycle.genesisTasks() : {};
339
354
  const list = all[stageKey] || [];
355
+ const taskIds = list.map((t) => t.id);
340
356
  const gov = s.government;
357
+ const nextOf = (key) => (key === 'legislation' ? null : fm.STAGES[fm.STAGES.indexOf(key) + 1]);
358
+ const closedAnswer = async (itemId, confirm) => {
359
+ const after = await reconcile(s);
360
+ return {
361
+ ok: true, closed: true, stage: stageKey,
362
+ next_stage: stageKey === 'legislation' ? null : (after && after.stage) || nextOf(stageKey),
363
+ item_id: itemId, confirm,
364
+ };
365
+ };
366
+ if (gov && typeof gov.closeGenesisStage === 'function') {
367
+ const r = await gov.closeGenesisStage({ stageKey, taskIds, closedBy: builderId });
368
+ if (r.reason) return { ok: false, reason: r.reason, unfinished: r.unfinished || [] };
369
+ const itemId = r.item ? String(r.item.id) : null;
370
+ if (r.closed) return closedAnswer(itemId, r.cast ? r.cast.cast_via : null);
371
+ return sittingOpen(stageKey, itemId, !r.opened, r.status, builderId);
372
+ }
341
373
  if (gov && typeof gov.openGenesisStageClose === 'function') {
342
- const r = await gov.openGenesisStageClose({ stageKey, taskIds: list.map((t) => t.id), openedBy: builderId });
374
+ // An older government module without the one-call close: the sitting opens and
375
+ // the answer says so, rather than claiming a close that did not happen.
376
+ const r = await gov.openGenesisStageClose({ stageKey, taskIds, openedBy: builderId });
343
377
  if (!r.opened && r.reason !== 'already_open') return { ok: false, reason: r.reason, unfinished: r.unfinished || [] };
344
- return { ok: true, item_id: r.item ? String(r.item.id) : null, already_open: !r.opened };
378
+ const board = await boardByStage(gov);
379
+ return sittingOpen(stageKey, r.item ? String(r.item.id) : null, !r.opened, board[stageKey] || null, builderId);
345
380
  }
346
381
  if (!list.length || !list.every((t) => FINISHED.includes(t.status))) return { ok: false, reason: 'stage_tasks_unfinished' };
347
382
  if (stageKey !== 'legislation') {
348
- await fm.setFoundingMode({ stage: fm.STAGES[fm.STAGES.indexOf(stageKey) + 1] }, builderId, modeDeps(s));
383
+ await fm.setFoundingMode({ stage: nextOf(stageKey) }, builderId, modeDeps(s));
349
384
  }
350
- return { ok: true, item_id: null, closed: true };
385
+ return { ok: true, closed: true, stage: stageKey, next_stage: nextOf(stageKey), item_id: null, confirm: null };
386
+ }
387
+
388
+ // The "a sitting is deciding it" answer: what happened, what it waits on, what next.
389
+ function sittingOpen(stageKey, itemId, alreadyOpen, status, builderId) {
390
+ const st = status || {};
391
+ const waitingOn = Array.isArray(st.waiting_on) ? st.waiting_on : [];
392
+ const names = waitingOn.map((m) => m.display_name || m.github_login || `builder ${m.builder_id}`);
393
+ const callerWaited = waitingOn.some((m) => String(m.builder_id) === String(builderId));
394
+ let next = `The Board Room is deciding this stage (sitting ${itemId || 'open'}).`;
395
+ if (st.needs) next += ` Its rule: ${st.needs}`;
396
+ if (names.length) next += ` Still to vote: ${names.join(', ')}.`;
397
+ if (callerWaited && itemId) next += ` Your vote is one of them: POST /api/bongos/government/board/items/${itemId}/votes { "direction": "yes" }.`;
398
+ next += ' The stage moves on by itself when the sitting passes; there is nothing else to call.';
399
+ return {
400
+ ok: false, closed: false, reason: 'sitting_open', stage: stageKey, item_id: itemId, already_open: !!alreadyOpen,
401
+ waiting: {
402
+ pass_rule: st.pass_rule || null, needs: st.needs || null,
403
+ voted: Array.isArray(st.voted) ? st.voted : [], waiting_on: waitingOn,
404
+ closes_at: st.closes_at || null, remaining_seconds: st.remaining_seconds ?? null,
405
+ },
406
+ next,
407
+ };
351
408
  }
352
409
 
353
410
  // comeAlive — the last door. Only once legislation has passed (or, with no Board Room,
@@ -170,6 +170,20 @@ function mountGenesisRoutes(router) {
170
170
  })) return;
171
171
  next();
172
172
  }, handle((req) => genesis.savePlan(req.body || {}, req.builder.id)));
173
- router.post('/founding/stages/:stage/close', api.requireBuilder, owns, handle((req) => genesis.closeStage(String(req.params.stage), req.builder.id)));
173
+ // task 1005121: one call. 200 { closed: true, next_stage } when the stage closed;
174
+ // 202 { ok: false, closed: false, reason: 'sitting_open', waiting, next } when a
175
+ // real sitting is deciding it — never a success for a stage that did not close.
176
+ router.post('/founding/stages/:stage/close', api.requireBuilder, owns, async (req, res) => {
177
+ try {
178
+ const r = await genesis.closeStage(String(req.params.stage), req.builder.id);
179
+ res.set('Cache-Control', 'no-store');
180
+ if (r && r.reason === 'sitting_open') return res.status(202).json(r);
181
+ if (r && r.ok === false) return refuse(res, r);
182
+ res.json(r);
183
+ } catch (err) {
184
+ api.logger('onboarding').error('[onboarding] genesis', err);
185
+ res.status(500).json({ error: { code: 'genesis_failed', message: 'internal error' } });
186
+ }
187
+ });
174
188
  router.post('/founding/alive', api.requireBuilder, owns, handle((req) => genesis.comeAlive(req.builder.id)));
175
189
  }
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.74",
3
+ "version": "1.24.75",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.74",
9
+ "version": "1.24.75",
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.24.74",
3
+ "version": "1.24.75",
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",
@@ -11469,5 +11469,11 @@
11469
11469
  "id": "1005142",
11470
11470
  "text": "The hall has a game mode: once it is on, every page opens full screen with no sidebar, and Escape brings the menus back when you need them."
11471
11471
  }
11472
+ ],
11473
+ "1.24.75": [
11474
+ {
11475
+ "id": "1005121",
11476
+ "text": "Closing a project-setup stage is now one step: if the founder alone decides, the stage closes and the project moves on at once; if others must vote, the answer says who and what to do next instead of falsely reporting success."
11477
+ }
11472
11478
  ]
11473
11479
  }
@@ -801,12 +801,12 @@ async function runWalk() {
801
801
  for (const stage of genesis.PROPER) {
802
802
  store.tasks.get(STAGE_TASK_IDS[stage]).status = 'shipped';
803
803
  let itemId = null;
804
- await check(`${stage}: its finished stage is put to the board`, 'a sitting under the chosen government', async () => {
804
+ await check(`${stage}: its finished stage is put to the board`, '202 not closed, a sitting under the chosen government', async () => {
805
805
  const r = await call(FOUNDER, 'POST', `/founding/stages/${stage}/close`);
806
806
  itemId = r.json && r.json.item_id;
807
807
  const snap = itemId && itemOf(itemId) ? itemOf(itemId).constitution : {};
808
- const reads = charter.readCharter(snap).form;
809
- return { got: `${r.status}, sitting under ${reads}`, pass: r.status === 200 && !!itemId && reads === CHOSEN };
808
+ const reads = charter.readCharter(snap).form, notClosed = !!r.json && r.json.closed === false && r.json.reason === 'sitting_open'; // task 1005121: never a 200 for an open stage
809
+ return { got: `${r.status} ${notClosed ? 'not closed' : 'closed?'}, a sitting under ${reads}`, pass: r.status === 202 && notClosed && !!itemId && reads === CHOSEN };
810
810
  });
811
811
  await check(`${stage}: the founder alone cannot close it; every trusted member's yes does`, 'open, open, passed', async () => {
812
812
  const after = await voteAll(itemId, timocrats);
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.74'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.75'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -205,8 +205,13 @@ test('decide → five stages through the Board Room → alive, and the hall open
205
205
  w.finish(s);
206
206
  assert.equal((await gh.readGenesis(w.deps())).can_close, true);
207
207
  const closed = await gh.closeStage(s, 1, w.deps());
208
- assert.equal(closed.ok, true);
208
+ // task 1005121: a module without the one-call close only OPENS a sitting, and
209
+ // the answer must say so — never ok: true for a stage that did not close.
210
+ assert.equal(closed.ok, false);
211
+ assert.equal(closed.closed, false);
212
+ assert.equal(closed.reason, 'sitting_open');
209
213
  assert.equal(closed.item_id, '9');
214
+ assert.match(closed.next, /moves on by itself/);
210
215
  assert.deepEqual(w.opened.at(-1), { stageKey: s, taskIds: [String(100 + STAGES5.indexOf(s))] }, 'the stage\'s own task ids go to the gate');
211
216
  g = await gh.readGenesis(w.deps());
212
217
  assert.equal(g.stage, s, 'sitting is not passing: the stage holds until the vote');
@@ -355,3 +360,83 @@ test('the hall carries the five life plates byte-identical to the world\'s', ()
355
360
  }
356
361
  }
357
362
  });
363
+
364
+ // ── task 1005121: closing a stage is ONE call ────────────────────────────────
365
+
366
+ function oneCallWorld({ solo = true } = {}) {
367
+ const w = world();
368
+ const calls = [];
369
+ w.deps = ((base) => () => {
370
+ const d = base();
371
+ d.government = {
372
+ ...d.government,
373
+ closeGenesisStage: async ({ stageKey, taskIds, closedBy }) => {
374
+ calls.push({ stageKey, taskIds, closedBy });
375
+ if (!(w.tasks[stageKey] || []).every((t) => ['shipped', 'abandoned'].includes(t.status))) {
376
+ return { closed: false, reason: 'stage_tasks_unfinished', unfinished: [{ task_id: taskIds[0] }] };
377
+ }
378
+ if (solo) {
379
+ w.board[stageKey] = { state: 'passed', item_id: '9', waiting_on: [] };
380
+ return { closed: true, item: { id: 9 }, stage: { key: stageKey }, cast: { cast_via: 'stage_close', fresh: true } };
381
+ }
382
+ const status = {
383
+ key: stageKey, state: 'sitting', item_id: '9', pass_rule: 'consent', needs: 'Passes unless someone objects.',
384
+ voted: [], waiting_on: [{ builder_id: '1', display_name: 'Founder' }, { builder_id: '2', github_login: 'avery' }],
385
+ closes_at: 'T', remaining_seconds: 600,
386
+ };
387
+ w.board[stageKey] = status;
388
+ return { closed: false, item: { id: 9 }, stage: { key: stageKey }, opened: true, status, cast: null };
389
+ },
390
+ };
391
+ return d;
392
+ })(w.deps);
393
+ return { w, calls };
394
+ }
395
+
396
+ test('task 1005121: one call closes the stage and moves the project on, at every stage, the same way', async () => {
397
+ const { w, calls } = oneCallWorld();
398
+ await gh.startFounding(1, w.deps());
399
+ await gh.savePlan({}, 1, w.deps());
400
+ for (const s of STAGES5) {
401
+ assert.equal((await gh.closeStage(s, 1, w.deps())).reason, 'stage_tasks_unfinished');
402
+ w.finish(s);
403
+ const r = await gh.closeStage(s, 1, w.deps());
404
+ assert.equal(r.ok, true, `${s}: one call`);
405
+ assert.equal(r.closed, true);
406
+ assert.equal(r.stage, s);
407
+ assert.equal(r.confirm, 'stage_close', "the founder's confirm carried it, and the answer says how");
408
+ const next = s === 'legislation' ? null : STAGES5[STAGES5.indexOf(s) + 1];
409
+ assert.equal(r.next_stage, next, 'the answer names the next stage');
410
+ const g = await gh.readGenesis(w.deps());
411
+ assert.equal(g.stage, next || 'legislation', 'no second request: the project has already moved on');
412
+ }
413
+ assert.ok(calls.every((c) => String(c.closedBy) === '1'), 'the caller is who confirms');
414
+ assert.equal((await gh.readGenesis(w.deps())).can_come_alive, true, 'legislation closed: the alive door is open');
415
+ });
416
+
417
+ test('task 1005121: where a real sitting decides, the answer is not ok and says what it waits on and what to do', async () => {
418
+ const { w } = oneCallWorld({ solo: false });
419
+ await gh.startFounding(1, w.deps());
420
+ await gh.savePlan({}, 1, w.deps());
421
+ w.finish('scope');
422
+ const r = await gh.closeStage('scope', 1, w.deps());
423
+ assert.equal(r.ok, false, 'never ok: true for a stage that did not close');
424
+ assert.equal(r.closed, false);
425
+ assert.equal(r.reason, 'sitting_open');
426
+ assert.equal(r.item_id, '9');
427
+ assert.equal(r.waiting.pass_rule, 'consent');
428
+ assert.deepEqual(r.waiting.waiting_on.map((m) => m.builder_id), ['1', '2']);
429
+ assert.match(r.next, /Founder, avery/);
430
+ assert.ok(r.next.includes('Your vote is one of them: POST /api/bongos/government/board/items/9/votes'), r.next);
431
+ assert.equal((await gh.readGenesis(w.deps())).stage, 'scope', 'the stage holds until the sitting passes');
432
+ });
433
+
434
+ test('task 1005121: the close route answers 202 with the whole body for an open sitting, never a 200', () => {
435
+ const src = fs.readFileSync(path.join(ROOT, 'modules', 'onboarding', 'routes', 'onboarding.js'), 'utf8');
436
+ const at = src.indexOf("router.post('/founding/stages/:stage/close'");
437
+ const route = src.slice(at, at + 700);
438
+ assert.ok(route.includes("r.reason === 'sitting_open') return res.status(202).json(r)"), 'an open sitting is a 202 with the whole body');
439
+ const hall = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'genesis-home.js'), 'utf8');
440
+ const click = hall.slice(hall.indexOf("t.hasAttribute('data-genesis-close')"), hall.indexOf("t.hasAttribute('data-genesis-confirm')"));
441
+ assert.ok(click.length > 0 && !click.includes('/votes'),'the hall no longer casts a second vote after closing — the server did it');
442
+ });
@@ -303,3 +303,86 @@ test('the port carries the open and the status; the status route is pinned on th
303
303
  const pins = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'), 'utf8');
304
304
  assert.match(pins, /'GET \/government\/board\/genesis-stages': 'perm:board\.vote\.cast'/);
305
305
  });
306
+
307
+ // ── task 1005121: closing a stage is ONE call ────────────────────────────────
308
+
309
+ const { closeGenesisStage } = require(path.join(GOV, 'genesis-close.js'));
310
+
311
+ test('task 1005121: under Monarchy / BDFL closeGenesisStage opens, confirms and CLOSES in one call, and records HOW', async () => {
312
+ const votes = [];
313
+ let karma = 0;
314
+ const passed = await capturing('board.genesis_stage.passed', () => withDb({
315
+ getTaskStatuses: async () => [{ id: '101', status: 'shipped' }],
316
+ openBoardItem: async (args) => ({ ...stageItem(), constitution: MONARCHY, opened_by: args.openedBy }),
317
+ getBoardItemForVote: async () => stageItem(),
318
+ builderHoldsRankActive: async (id) => id === FOUNDER,
319
+ getActiveBuildersWithRank: async () => [member(FOUNDER, 'founder')],
320
+ getBoardItemAuthor: async (item) => item.opened_by,
321
+ insertBoardVote: async (row) => { const v = { ...row, voter_id: row.voterId, cast_via: row.castVia }; votes.push(v); return v; },
322
+ insertVoteKarma: async () => { karma += 1; },
323
+ getVotesForItem: async () => votes,
324
+ closeBoardItem: async ({ outcome }) => ({ ...stageItem(), closed_at: 'T', outcome }),
325
+ insertAuthorRatifyKarma: async () => true,
326
+ }, async () => {
327
+ const out = await closeGenesisStage({ stageKey: 'charter', taskIds: [101], closedBy: FOUNDER });
328
+ assert.equal(out.closed, true, 'the answer says CLOSED — no second request needed');
329
+ assert.equal(out.stage.key, 'charter');
330
+ assert.equal(out.cast.cast_via, 'stage_close');
331
+ }));
332
+ assert.equal(votes.length, 1);
333
+ assert.equal(votes[0].castVia, 'stage_close', 'the vote row records it was a confirm given by closing the stage');
334
+ assert.equal(votes[0].direction, 'yes');
335
+ assert.equal(karma, 0, 'the confirm decided nothing the ask had not — no vote karma');
336
+ assert.equal(passed.length, 1, 'the genesis home hears the pass');
337
+ });
338
+
339
+ test('task 1005121: where one yes cannot carry it, nothing is cast and the answer says what the sitting waits on', async () => {
340
+ let inserted = 0;
341
+ await withDb({
342
+ getTaskStatuses: async () => [{ id: '101', status: 'shipped' }],
343
+ openBoardItem: async () => ({ ...stageItem({ constitution: CONSENT }) }),
344
+ insertBoardVote: async () => { inserted += 1; return null; },
345
+ listLatestGenesisStageItems: async () => [stageItem({ constitution: CONSENT })],
346
+ getVotesForItems: async () => [],
347
+ getActiveBuildersWithRank: async () => [member(FOUNDER, 'founder'), member(AVERY, 'avery')],
348
+ }, async () => {
349
+ const out = await closeGenesisStage({ stageKey: 'charter', taskIds: [101], closedBy: FOUNDER });
350
+ assert.equal(out.closed, false);
351
+ assert.equal(out.opened, true);
352
+ assert.equal(out.item.id, '90');
353
+ assert.equal(out.status.state, 'sitting');
354
+ assert.deepEqual(out.status.waiting_on.map((m) => m.builder_id), [FOUNDER, AVERY]);
355
+ assert.ok(out.status.needs, 'the rule, in words');
356
+ });
357
+ assert.equal(inserted, 0, 'no vote is cast on anyone\'s behalf under a rule one person cannot carry');
358
+ });
359
+
360
+ test('task 1005121: a refusal from the gate passes straight through, nothing opened', async () => {
361
+ await withDb({
362
+ getTaskStatuses: async () => [{ id: '101', status: 'active' }],
363
+ openBoardItem: async () => { throw new Error('must not be reached'); },
364
+ }, async () => {
365
+ const out = await closeGenesisStage({ stageKey: 'charter', taskIds: [101], closedBy: FOUNDER });
366
+ assert.equal(out.closed, false);
367
+ assert.equal(out.reason, 'stage_tasks_unfinished');
368
+ });
369
+ });
370
+
371
+ test('task 1005121: cast_via stage_close is internal, genesis-only and yes-only; the vote route never passes it', async () => {
372
+ assert.equal((await castVote({ itemId: '90', voterId: FOUNDER, direction: 'yes', castVia: 'proxy' })).reason, 'bad_cast_via');
373
+ await withDb({
374
+ getBoardItemForVote: async () => ({ ...stageItem(), subject_type: 'full_idea' }),
375
+ insertBoardVote: async () => { throw new Error('must not be reached'); },
376
+ }, async () => {
377
+ assert.equal((await castVote({ itemId: '90', voterId: FOUNDER, direction: 'yes', castVia: 'stage_close' })).reason, 'bad_cast_via');
378
+ });
379
+ const routes = fs.readFileSync(path.join(GOV, 'routes', 'government.js'), 'utf8');
380
+ const call = routes.slice(routes.indexOf('const out = await board.castVote({'), routes.indexOf('const out = await board.castVote({') + 300);
381
+ assert.doesNotMatch(call, /castVia|cast_via/, 'a request can never claim its vote was a stage-close confirm');
382
+ assert.ok(routes.includes("closeGenesisStage: require('../genesis-close').closeGenesisStage"), 'the one-call close is on the port');
383
+ const sql = fs.readFileSync(path.join(GOV, 'migrations', 'government_038_vote_cast_via.sql'), 'utf8');
384
+ assert.match(sql, /cast_via text NOT NULL DEFAULT 'ballot'/, 'every vote before the column reads as a ballot');
385
+ assert.match(sql, /cast_via IN \('ballot', 'stage_close'\) AND \(cast_via = 'ballot' OR direction = 'yes'\)/);
386
+ const dbSrc = fs.readFileSync(path.join(GOV, 'db.js'), 'utf8');
387
+ assert.match(dbSrc, /INSERT INTO government_board_votes \(item_id, voter_id, direction, reason_md, section_key, cast_via\)/);
388
+ });
@@ -54,6 +54,7 @@ const NON_GRANT_MIGRATIONS = new Set([
54
54
  'government_032_ask_flags.sql', // the reword list: matters a reader flagged as unclear (owner 2026-10-23)
55
55
  'government_036_access_holders.sql', // who holds the keys outside Bongos + the access:<key> ask seat (task 1005000)
56
56
  'government_037_constitution.sql', // the constitution in force as a durable row (task 1005140)
57
+ 'government_038_vote_cast_via.sql', // HOW a vote was cast: a ballot, or a stage-close confirm (task 1005121)
57
58
  ]);
58
59
 
59
60
  // Permission keys RENAMED after the grant migrations that first granted them.