@bongos/core 1.21.120 → 1.21.121

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.21.120",
6
- "core_contract": "1.21.120",
7
- "source_commit": "a0abf3fc6c913d358ffd525f1592603c5ba771b0",
5
+ "core_version": "1.21.121",
6
+ "core_contract": "1.21.121",
7
+ "source_commit": "514ed742253be1dacb6b1d18d49f2f6fa3f6b427",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-14T16:25:27.328Z",
9
+ "built_at": "2026-10-14T17:15:08.661Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 605,
13
13
  "agent_docs_stubbed": 28,
14
- "functional_verbatim": 3117,
14
+ "functional_verbatim": 3118,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3751,
20
- "tree_sha256": "9223a47c6d78a1edb87bbb0565101cac2b5d3c655b3cc9a703bee7c6116db5be",
19
+ "file_count": 3752,
20
+ "tree_sha256": "49b303e76f9a422c8c0b909fe0ee46687b713859b18104045df85b7ef48fa505",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -337,7 +337,7 @@
337
337
  {
338
338
  "path": "clients/bongos-client/index.d.ts",
339
339
  "mode": "0000644",
340
- "sha256": "180a7c8a717b39b948341c1f0e5fb9bf69aec98f5d1f8cf1c37607972a05ed6b"
340
+ "sha256": "493cebcc4535646738cfda7d3df73ba9895e7e88c3c268d62cca55420aebab29"
341
341
  },
342
342
  {
343
343
  "path": "clients/bongos-client/index.mjs",
@@ -1462,7 +1462,7 @@
1462
1462
  {
1463
1463
  "path": "docs/adr/0199-restart-an-unreachable-active-project.md",
1464
1464
  "mode": "0000644",
1465
- "sha256": "999c2eb382b51317a4f1fbf7a615855be7baeded04cf03c4e1a411b2f98386cd"
1465
+ "sha256": "a1dae8f4126ab9cfff7390bbde17a501ec0fe0b17f5c49ce2d0b4bd4e7735d9b"
1466
1466
  },
1467
1467
  {
1468
1468
  "path": "docs/adr/0200-re-point-a-bring-your-own-domain.md",
@@ -2382,7 +2382,7 @@
2382
2382
  {
2383
2383
  "path": "docs/api/openapi.json",
2384
2384
  "mode": "0000644",
2385
- "sha256": "6ecf7c560ed5131cbc24a55498493a1fcf200516118c4c84e4ef0484f1d780e7"
2385
+ "sha256": "1504a1bc002e033dd4c69c55a074014b35f88fe0015746911ac69e02cff4a430"
2386
2386
  },
2387
2387
  {
2388
2388
  "path": "docs/architecture.md",
@@ -2407,12 +2407,12 @@
2407
2407
  {
2408
2408
  "path": "docs/copy-inventory.md",
2409
2409
  "mode": "0000644",
2410
- "sha256": "32112bc332f327d446a1a066fc4036a916ca7e1a021202d4c07f468973910697"
2410
+ "sha256": "2f6c0fff979e7a52a1dff20184291da110751084197f9619497bc5a8668c2c0f"
2411
2411
  },
2412
2412
  {
2413
2413
  "path": "docs/copy-registry.json",
2414
2414
  "mode": "0000644",
2415
- "sha256": "50acc9b093e07c52ee9fc4b31a3e69c5ae6bc73c00530c71fc4a81c6f6aca727"
2415
+ "sha256": "2fa52a4d4411a29e5aadeb0a03898a32144834527c7e84804f4c9df5f584cab6"
2416
2416
  },
2417
2417
  {
2418
2418
  "path": "docs/design/apex-pass-2-direction.md",
@@ -2922,7 +2922,7 @@
2922
2922
  {
2923
2923
  "path": "docs/module-api-changelog.md",
2924
2924
  "mode": "0000644",
2925
- "sha256": "72ce941af880f14ccd6a7b1df337934e527a8553d85cbe8b986076bbcf1358cc"
2925
+ "sha256": "24cea3fdb2f8e21b6358090612695e37c03e734fcd305e87d35456fa8a515adf"
2926
2926
  },
2927
2927
  {
2928
2928
  "path": "docs/modules-contract.md",
@@ -3012,7 +3012,7 @@
3012
3012
  {
3013
3013
  "path": "docs/page-readings.json",
3014
3014
  "mode": "0000644",
3015
- "sha256": "cda2d7936c5a440c9bf7a535d553c316ee41271a7081b283d52d1599f605d052"
3015
+ "sha256": "d59a98a2c166e276275156dfb13887c9147f1596ca65fb777c97e988dd23bf25"
3016
3016
  },
3017
3017
  {
3018
3018
  "path": "docs/project-context.template.md",
@@ -6132,7 +6132,7 @@
6132
6132
  {
6133
6133
  "path": "modules/hall-ui/public/modules.js",
6134
6134
  "mode": "0000644",
6135
- "sha256": "6f960241607ea3921cce5d40bf5fbb6d17595d26213601cbc6a7195448624b11"
6135
+ "sha256": "a42b39265d8cfc1a191fe203c6d48c7da111e231ae33da731ebe5a9aa7cd656c"
6136
6136
  },
6137
6137
  {
6138
6138
  "path": "modules/hall-ui/public/modules.states.json",
@@ -8782,7 +8782,7 @@
8782
8782
  {
8783
8783
  "path": "modules/provisioning/provisioning.js",
8784
8784
  "mode": "0000644",
8785
- "sha256": "b45bcb38497d8999ea0e5e3d0844785a7514a829cfd6cbb58c9b3167ccce37f9"
8785
+ "sha256": "c99cfa7d071ece3f6d821709a8610baa25bb0323f7219016ac53d7e8ebb5e197"
8786
8786
  },
8787
8787
  {
8788
8788
  "path": "modules/provisioning/public-refusal.js",
@@ -9667,7 +9667,7 @@
9667
9667
  {
9668
9668
  "path": "modules/public-landing/public/projects.html",
9669
9669
  "mode": "0000644",
9670
- "sha256": "060c9904c6fefb2848819609a61bbb76822c273c601d670500a9e031380120e1"
9670
+ "sha256": "ead1660e07f5bd8cf34008318e1c729ce39f5f4717e104be4cd2e64942a6b264"
9671
9671
  },
9672
9672
  {
9673
9673
  "path": "modules/public-landing/public/projects.probes.json",
@@ -10467,12 +10467,12 @@
10467
10467
  {
10468
10468
  "path": "package-lock.json",
10469
10469
  "mode": "0000644",
10470
- "sha256": "acf5cb64360f149851f33d37eb868b75ea6a476b6a26fe8c7fba494d0a530202"
10470
+ "sha256": "4fbad4e149e09def5dfaf46fb9069c0298871200c54d8836c61efdf527e14038"
10471
10471
  },
10472
10472
  {
10473
10473
  "path": "package.json",
10474
10474
  "mode": "0000644",
10475
- "sha256": "0e819e620052f3e27e6088f87839bfa0ac850b3b1992024fd13c5540930cafce"
10475
+ "sha256": "7107e720fbe7a367451fb610dcccac24f7563e1a50b898bf1899b76afc53fd6f"
10476
10476
  },
10477
10477
  {
10478
10478
  "path": "public-docs/index.html",
@@ -10492,7 +10492,7 @@
10492
10492
  {
10493
10493
  "path": "release-notes.json",
10494
10494
  "mode": "0000644",
10495
- "sha256": "6969dc70a1c0541fcd0a79f7b801d7de58b8a84b96c22e8e36b5524432878219"
10495
+ "sha256": "7818da891dbc2d601d13dc938d43d83f49061084ed93fef83fe6d4479c98f828"
10496
10496
  },
10497
10497
  {
10498
10498
  "path": "scripts/cron/style_guide_audit.sh",
@@ -11002,7 +11002,7 @@
11002
11002
  {
11003
11003
  "path": "scripts/gds/exec-path-guard.js",
11004
11004
  "mode": "0000644",
11005
- "sha256": "8ed7c35c0d6be3c8a11ee990b487114d7e422caeb70c67240b5060a0087cfdc4"
11005
+ "sha256": "cda9e125cfb2cb354c7d4e20065c36712a75955d3981203012c2334d0ce637ae"
11006
11006
  },
11007
11007
  {
11008
11008
  "path": "scripts/gds/feedback-send.js",
@@ -11587,7 +11587,7 @@
11587
11587
  {
11588
11588
  "path": "scripts/gds/provision.js",
11589
11589
  "mode": "0000644",
11590
- "sha256": "ff2d4e3576942d20056d11236dc13481a8ef560fa9828b691b30ba3758203048"
11590
+ "sha256": "ea11cc49dbc298c8b3ed4ad88e25a3acfbba65fcd65727c9b47c43ee41e24437"
11591
11591
  },
11592
11592
  {
11593
11593
  "path": "scripts/gds/publish-credential-check.js",
@@ -12722,7 +12722,7 @@
12722
12722
  {
12723
12723
  "path": "src/bongos/routes/modules.js",
12724
12724
  "mode": "0000644",
12725
- "sha256": "aea0a0f28621500bee51e4fec8e794139cf90c3360fdc55caecbbd5a5fc82b69"
12725
+ "sha256": "e6b1b25371695b4aa959f5fdcece31869e827221f0205d1797c8332628d7b6a2"
12726
12726
  },
12727
12727
  {
12728
12728
  "path": "src/bongos/routes/my-sessions.js",
@@ -12802,7 +12802,7 @@
12802
12802
  {
12803
12803
  "path": "src/module-api.js",
12804
12804
  "mode": "0000644",
12805
- "sha256": "ea34607161a0fbc072ecd805d5e150692df9557f574b6c74ebd53f96ea8ba341"
12805
+ "sha256": "0ebfd076b4e28ae56b54b3bdc1f43d1f4ee3e74391b4b28600de50f653ee097c"
12806
12806
  },
12807
12807
  {
12808
12808
  "path": "src/module-loader/catalog.js",
@@ -12812,7 +12812,7 @@
12812
12812
  {
12813
12813
  "path": "src/module-loader/loader.js",
12814
12814
  "mode": "0000644",
12815
- "sha256": "f3ec8f005fb9bb366a489b82a5836195b78982b0f605a8e03b588f53f7f9392f"
12815
+ "sha256": "04458c645d876011324e5ae11011774699c06e39ff2fa1ba1cf58a2d2775f887"
12816
12816
  },
12817
12817
  {
12818
12818
  "path": "src/module-loader/manifest-schema.js",
@@ -15267,7 +15267,7 @@
15267
15267
  {
15268
15268
  "path": "tests/hall_catalog_world.mjs",
15269
15269
  "mode": "0000644",
15270
- "sha256": "11f0ac50c5424a76371591b31275c796ded853db0cd35798b9adc878cdd88c88"
15270
+ "sha256": "8aad2dd0521d212dfdb21a4fe4d2c341ddc94796bd4bc9b871ac1b5b25cc596d"
15271
15271
  },
15272
15272
  {
15273
15273
  "path": "tests/hall_city.mjs",
@@ -16362,7 +16362,7 @@
16362
16362
  {
16363
16363
  "path": "tests/module_loader.mjs",
16364
16364
  "mode": "0000644",
16365
- "sha256": "88100737a4b7a67899c3eba3dc01bcf3079d4171ad1dad62b79ffdb49ec3bee3"
16365
+ "sha256": "a2a029afd86336a83deeb79f0856473470024fa94da01e1c556094b8e6d042d5"
16366
16366
  },
16367
16367
  {
16368
16368
  "path": "tests/module_manifest.mjs",
@@ -16434,6 +16434,11 @@
16434
16434
  "mode": "0000644",
16435
16435
  "sha256": "0db12d802203780f489e9a6d7abf970dfcde9099ac7011fcaaa6c575336804cd"
16436
16436
  },
16437
+ {
16438
+ "path": "tests/module_toggle_restart.mjs",
16439
+ "mode": "0000644",
16440
+ "sha256": "31b96e448b751a6f81891286e0dee96eb13e543d029787c5520f785fe2f45291"
16441
+ },
16437
16442
  {
16438
16443
  "path": "tests/module_update.mjs",
16439
16444
  "mode": "0000644",
@@ -17017,7 +17022,7 @@
17017
17022
  {
17018
17023
  "path": "tests/projects_hub_pre_uat.mjs",
17019
17024
  "mode": "0000644",
17020
- "sha256": "e1595e83c5ee16acf89d4dcbe2771e09f2e35a09c8e099d048857932388ba571"
17025
+ "sha256": "d18b4f99bb89f935d594020df107c7e7a4db3ea1075a4b48dce33b7312412bb8"
17021
17026
  },
17022
17027
  {
17023
17028
  "path": "tests/projects_hub_render_connect.mjs",
@@ -17102,7 +17107,7 @@
17102
17107
  {
17103
17108
  "path": "tests/provision_restart.mjs",
17104
17109
  "mode": "0000644",
17105
- "sha256": "caa345e6708d896b42c597a9f83919b4795222e61955c728975d56a873a04fb8"
17110
+ "sha256": "deb9ee70fd9fdc70b25a5dd97ed76200ab10daa7eef3f6ea61bb6d5b7bb50494"
17106
17111
  },
17107
17112
  {
17108
17113
  "path": "tests/provision_settings_apply.mjs",
@@ -17232,7 +17237,7 @@
17232
17237
  {
17233
17238
  "path": "tests/provisioning_restart.mjs",
17234
17239
  "mode": "0000644",
17235
- "sha256": "c591f3fcf47551575ff99a7da62dc9c68763698bed7144d463a9b11a37d34ee0"
17240
+ "sha256": "5cd2eda5b5e7dd09e96694f7973672c557daa8c8c5197e4dc2ee90eaa30dff21"
17236
17241
  },
17237
17242
  {
17238
17243
  "path": "tests/provisioning_runner_drift_notice.mjs",
@@ -17247,7 +17252,7 @@
17247
17252
  {
17248
17253
  "path": "tests/provisioning_settings_apply.mjs",
17249
17254
  "mode": "0000644",
17250
- "sha256": "432e7122f9ab59de28f46d9bdc342c1387a799b2b567c26c1d019b57ae40ac92"
17255
+ "sha256": "2a5645624375b0d4386e12af1e437bdefc41b60d0d3f0b7b2c1a6c53f6a2e06f"
17251
17256
  },
17252
17257
  {
17253
17258
  "path": "tests/provisioning_settings_env.mjs",
@@ -440,7 +440,7 @@ export interface PostMinglePairsIdDecideResponse { pair: unknown }
440
440
  export interface PostMingleRoundRequest { limit?: number }
441
441
  export interface PostMingleRoundResponse { paired: unknown; count: unknown; skipped: unknown }
442
442
  export interface PostModulesKeyEnableRequest { enabled?: unknown }
443
- export interface PostModulesKeyEnableResponse { ok: boolean; key: unknown; enabled: unknown; effective: unknown; source: unknown; note: unknown }
443
+ export interface PostModulesKeyEnableResponse { ok: boolean; key: unknown; enabled: unknown; effective: unknown; source: unknown; restart_pending: unknown; note: unknown }
444
444
  export interface PostModulesKeySubmitRequest { signed_by?: string }
445
445
  export interface PostModulesKeySubmitResponse { ok: boolean; submission: unknown }
446
446
  export interface PostMyProjectsClientIdLeaveResponse { ok: boolean; left: unknown }
@@ -47,3 +47,7 @@ None of them came with an action. The pre-UAT hub review (2026-08-24) recorded w
47
47
  - **Reporting `healthy:true` as "answering again" on a domained instance** — the loopback probe cannot see what the sweep sees; the card would still say unreachable and the event would read as a lie.
48
48
  - **Retrying a restart that left the project down** — restarting a broken instance four more times fixes nothing and ends by overwriting the owner's action with an `error_note`.
49
49
  - **Showing the door on every active card** — an always-available Restart invites downtime on projects that are fine, and there is no reading behind it to act on.
50
+
51
+ ## Amendment (task 1004463, 2026-10-14)
52
+
53
+ The platform's own row (`control-plane`) joins `RESTART_SHAPES`, and decision 7's door shows on it whenever it is `active` with nothing in flight, healthy or not. It is the one exception to "no Restart door on every active card": a module switched on or off in that hall mounts its routes and starts its pollers only at the next restart (the Modules toggle now says so, `restartPendingFor` in `src/module-loader/loader.js`), and the owner's only other way to restart the control plane was a shell on the box. `control-plane-guard.js` already allowed `restart` on that row; the runner routes it to the control-plane executor as it does `settings-apply`.
@@ -32561,6 +32561,7 @@
32561
32561
  "enabled": {},
32562
32562
  "effective": {},
32563
32563
  "source": {},
32564
+ "restart_pending": {},
32564
32565
  "note": {}
32565
32566
  },
32566
32567
  "required": [
@@ -32569,6 +32570,7 @@
32569
32570
  "enabled",
32570
32571
  "effective",
32571
32572
  "source",
32573
+ "restart_pending",
32572
32574
  "note"
32573
32575
  ]
32574
32576
  },
@@ -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
- **2720 strings** across 3 surfaces · 2300 distinct pieces of copy · 2720 certain, 0 heuristic · 240 shown in more than one place.
7
+ **2722 strings** across 3 surfaces · 2302 distinct pieces of copy · 2722 certain, 0 heuristic · 240 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` | 143 | 1912 |
13
+ | `builders-hall` | `modules/hall-ui/public` | 143 | 1914 |
14
14
  | `status-dashboard` | `modules/status-ui/public` | 4 | 100 |
15
15
  | `landing` | `modules/public-landing/public` | 9 | 708 |
16
16
 
@@ -78,7 +78,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
78
78
  | `8226c979db4d` | 3x | Not now | modules/hall-ui/public/collab.js:668, modules/hall-ui/public/paintbrush.js:119, modules/hall-ui/public/paintbrush.js:183 |
79
79
  | … | | _200 more in the JSON registry_ | |
80
80
 
81
- ## `builders-hall` — 1912 strings
81
+ ## `builders-hall` — 1914 strings
82
82
 
83
83
  | id | conf | text | where |
84
84
  |---|---|---|---|
@@ -356,7 +356,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
356
356
  | `90691bceaf68` | certain | Could not load | `modules/hall-ui/public/project-settings.js:284` |
357
357
  | `90691bceaf68` | certain | Could not load | `modules/hall-ui/public/task.js:1022` |
358
358
  | `589a4cfc5442` | certain | Could not load idea {…}. | `modules/hall-ui/public/ideas.js:1018` |
359
- | `192c893fc64e` | certain | Could not load modules. | `modules/hall-ui/public/modules.js:335` |
359
+ | `192c893fc64e` | certain | Could not load modules. | `modules/hall-ui/public/modules.js:361` |
360
360
  | `9912c3835136` | certain | Could not load requests — reload to try again. | `modules/hall-ui/public/collab.js:845` |
361
361
  | `7775fec0fa85` | certain | Could not load sessions. | `modules/hall-ui/public/sessions.js:504` |
362
362
  | `7775fec0fa85` | certain | Could not load sessions. | `modules/hall-ui/public/sessions.js:542` |
@@ -376,7 +376,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
376
376
  | `64f9d51f0fa2` | certain | Could not load your settings. | `modules/hall-ui/public/settings.js:1199` |
377
377
  | `de244c45d916` | certain | Could not load {…} | `modules/hall-ui/public/task.js:1015` |
378
378
  | `2366356926b4` | certain | Could not mark the primer just now. Try again. | `modules/hall-ui/public/primer.js:259` |
379
- | `753987b5da13` | certain | Could not reach the submit endpoint. | `modules/hall-ui/public/modules.js:314` |
379
+ | `753987b5da13` | certain | Could not reach the submit endpoint. | `modules/hall-ui/public/modules.js:340` |
380
380
  | `6f500726f0ac` | certain | Could not read the report: {…} | `modules/hall-ui/public/watch.js:768` |
381
381
  | `21c2f33168a3` | certain | Could not read this project's deploy state: {…} | `modules/hall-ui/public/deploy.js:624` |
382
382
  | `21a782f1af26` | certain | Could not read what this would bring in: {…}. The check below still applies. | `modules/hall-ui/public/deploy.js:542` |
@@ -877,7 +877,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
877
877
  | `43626cff7071` | certain | Nothing of that kind in your sky. | `modules/hall-ui/public/sky-panel.js:123` |
878
878
  | `b05a5f8cb074` | certain | Nothing personal is sent. | `modules/hall-ui/public/my-agents.js:429` |
879
879
  | `ad9a17a8a5eb` | certain | Nothing shipped yet. | `modules/hall-ui/public/profile.js:297` |
880
- | `f326632f2013` | certain | Nothing to submit — every module here ships with the core. | `modules/hall-ui/public/modules.js:275` |
880
+ | `f326632f2013` | certain | Nothing to submit — every module here ships with the core. | `modules/hall-ui/public/modules.js:301` |
881
881
  | `27c0d872db2d` | certain | Nothing you have typed above is lost by opening this. | `modules/hall-ui/public/ideas.html:155` |
882
882
  | `26a07b64f49d` | certain | Now running core {…} | `modules/hall-ui/public/deploy.js:400` |
883
883
  | `21cb60a8a96a` | certain | Now running core {…} — but GitHub was not updated. | `modules/hall-ui/public/deploy.js:390` |
@@ -1761,6 +1761,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1761
1761
  | `9eca058c6ad2` | certain | release a thought — one line | `modules/hall-ui/public/goal-map.html:90` |
1762
1762
  | `9eca058c6ad2` | certain | release a thought — one line | `modules/hall-ui/public/thinking.html:98` |
1763
1763
  | `d4bb81532896` | certain | repro steps, context, anything that helps… | `modules/hall-ui/public/ideas.html:80` |
1764
+ | `299dd9d33f53` | certain | restart it | `modules/hall-ui/public/modules.js:129` |
1764
1765
  | `58b4e7a48d86` | certain | reviews from before Tweak Mode | `modules/hall-ui/public/studio.html:143` |
1765
1766
  | `0d677ab4868a` | certain | reviews written on the live site | `modules/hall-ui/public/studio.html:138` |
1766
1767
  | `f37f5043f81b` | certain | ring {…} of {…} · {…} | `modules/hall-ui/public/sky-interaction.js:34` |
@@ -1784,7 +1785,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1784
1785
  | `c8c2551006dc` | certain | skip the tour | `modules/hall-ui/public/goal-map.html:121` |
1785
1786
  | `c8c2551006dc` | certain | skip the tour | `modules/hall-ui/public/thinking.html:122` |
1786
1787
  | `1e737a57eb20` | certain | start governing | `modules/hall-ui/public/city.html:99` |
1787
- | `0215b2cdca8c` | certain | submit for review | `modules/hall-ui/public/modules.js:149` |
1788
+ | `0215b2cdca8c` | certain | submit for review | `modules/hall-ui/public/modules.js:175` |
1788
1789
  | `43e6f1592ae3` | certain | submit it in the editor | `modules/hall-ui/public/studio.js:829` |
1789
1790
  | `9ec19e32f86f` | certain | submit page | `modules/hall-ui/public/tweak-editor.html:68` |
1790
1791
  | `da30b952a60b` | certain | system / unattributed | `modules/hall-ui/public/goals-page.js:284` |
@@ -1852,6 +1853,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1852
1853
  | `8cd3cc4005fc` | certain | via goal | `modules/hall-ui/public/dom-utils.js:441` |
1853
1854
  | `007b28a56031` | certain | view diff | `modules/hall-ui/public/gate.js:160` |
1854
1855
  | `18cf722bdd7e` | certain | waiting · | `modules/hall-ui/public/sky-interaction.js:128` |
1856
+ | `354861d3c2ce` | certain | waits for a restart | `modules/hall-ui/public/modules.js:127` |
1855
1857
  | `35be9e0923da` | certain | walks the whole path in plain language. | `modules/hall-ui/public/hall-render.js:837` |
1856
1858
  | `db42054582ed` | certain | wells · | `modules/hall-ui/public/sky-interaction.js:129` |
1857
1859
  | `17705daf7b85` | certain | what counts as passing | `modules/hall-ui/public/government.html:55` |
@@ -1886,7 +1888,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1886
1888
  | `6c08bfe92e2b` | certain | {…} This project is invite-only: the {…} invites each builder, and there is nothing to apply for here. | `modules/hall-ui/public/hall-render.js:292` |
1887
1889
  | `c5623d60015d` | certain | {…} Your standing is {…}. | `modules/hall-ui/public/hall-render.js:850` |
1888
1890
  | `a91e1e702d4d` | certain | {…} achieved, {…} in progress, {…} not started of {…} goals | `modules/hall-ui/public/roadmap.js:73` |
1889
- | `a65d371b5cb3` | certain | {…} and above can enable or disable a module | `modules/hall-ui/public/modules.js:147` |
1891
+ | `a65d371b5cb3` | certain | {…} and above can enable or disable a module | `modules/hall-ui/public/modules.js:173` |
1890
1892
  | `98b1c674fc37` | certain | {…} and up | `modules/hall-ui/public/sessions.js:289` |
1891
1893
  | `111b71a9acaf` | certain | {…} and up. | `modules/hall-ui/public/sessions.js:287` |
1892
1894
  | `56b00d773232` | certain | {…} and {…} | `modules/hall-ui/public/profile-nudge.js:77` |
@@ -1924,7 +1926,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1924
1926
  | `1a8a6267e490` | certain | {…} unfinished task{…} — say what happens to each | `modules/hall-ui/public/goals-render.js:1042` |
1925
1927
  | `2f75f69cdb2f` | certain | {…} {…} changed | `modules/hall-ui/public/profile-crafts.js:218` |
1926
1928
  | `8186b562553a` | certain | {…} {…} waiting | `modules/hall-ui/public/work.js:420` |
1927
- | `a9216af9b05e` | certain | {…} {…} — how-to | `modules/hall-ui/public/modules.js:390` |
1929
+ | `a9216af9b05e` | certain | {…} {…} — how-to | `modules/hall-ui/public/modules.js:416` |
1928
1930
  | `0d4016a5531c` | certain | {…} {…}{…} | `modules/hall-ui/public/watch.js:514` |
1929
1931
  | `0d4016a5531c` | certain | {…} {…}{…} | `modules/hall-ui/public/work.js:265` |
1930
1932
  | `b3757ba3bb46` | certain | {…} · Stored encrypted; never shown again after saving. | `modules/hall-ui/public/settings.js:873` |
@@ -1932,7 +1934,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1932
1934
  | `d73256fbc395` | certain | {…} · who votes, and how it {…} | `modules/hall-ui/public/board-room.js:460` |
1933
1935
  | `cab2a4296aec` | certain | {…} · {…} · {…}{…} | `modules/hall-ui/public/goal-inbox.js:458` |
1934
1936
  | `1ac723849449` | certain | {…} · {…}{…} | `modules/hall-ui/public/sky-interaction.js:33` |
1935
- | `ddd2bfc1fb60` | certain | {…} — how to use it | `modules/hall-ui/public/modules.js:392` |
1937
+ | `ddd2bfc1fb60` | certain | {…} — how to use it | `modules/hall-ui/public/modules.js:418` |
1936
1938
  | `085f93f3f775` | certain | {…} — see all standings | `modules/hall-ui/public/hall-render.js:80` |
1937
1939
  | `2a2728a4ec4f` | certain | {…}% of tasks complete | `modules/hall-ui/public/roadmap.js:63` |
1938
1940
  | `15e545eb3ce2` | certain | {…}, no such task | `modules/hall-ui/public/task.js:1002` |
@@ -1947,7 +1949,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1947
1949
  | `97e27ab169ae` | certain | {…}{…} session{…} across {…} builder{…}. | `modules/hall-ui/public/sessions.js:443` |
1948
1950
  | `3d74668f69e5` | certain | {…}{…} · {…} | `modules/hall-ui/public/task.js:639` |
1949
1951
  | `7f79bedecf60` | certain | {…}{…} — open it to see what went | `modules/hall-ui/public/goal-inbox.js:374` |
1950
- | `1026fd5f68b7` | certain | · version | `modules/hall-ui/public/modules.js:395` |
1952
+ | `1026fd5f68b7` | certain | · version | `modules/hall-ui/public/modules.js:421` |
1951
1953
  | `4a5ff4f662df` | certain | — ${…} of ${…} this month ({…}%). {…} | `modules/hall-ui/public/hall-render.js:523` |
1952
1954
  | `a6ae9cbdee30` | certain | — all builders — | `modules/hall-ui/public/builders.js:711` |
1953
1955
  | `729d4ce22b99` | certain | — any — | `modules/hall-ui/public/hall-widgets.js:163` |
@@ -3,9 +3,9 @@
3
3
  "generator": "scripts/gds/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 2720,
7
- "unique_ids": 2300,
8
- "certain": 2720,
6
+ "strings": 2722,
7
+ "unique_ids": 2302,
8
+ "certain": 2722,
9
9
  "heuristic": 0,
10
10
  "duplicate_ids": 240
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
15
  "files": 143,
16
- "strings": 1912
16
+ "strings": 1914
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -5091,7 +5091,7 @@
5091
5091
  "surface": "builders-hall",
5092
5092
  "text": "Could not load modules.",
5093
5093
  "file": "modules/hall-ui/public/modules.js",
5094
- "line": 335,
5094
+ "line": 361,
5095
5095
  "origin": "js-notice",
5096
5096
  "confidence": "certain"
5097
5097
  },
@@ -5271,7 +5271,7 @@
5271
5271
  "surface": "builders-hall",
5272
5272
  "text": "Could not reach the submit endpoint.",
5273
5273
  "file": "modules/hall-ui/public/modules.js",
5274
- "line": 314,
5274
+ "line": 340,
5275
5275
  "origin": "js-notice",
5276
5276
  "confidence": "certain"
5277
5277
  },
@@ -9780,7 +9780,7 @@
9780
9780
  "surface": "builders-hall",
9781
9781
  "text": "Nothing to submit — every module here ships with the core.",
9782
9782
  "file": "modules/hall-ui/public/modules.js",
9783
- "line": 275,
9783
+ "line": 301,
9784
9784
  "origin": "js-text-assign",
9785
9785
  "confidence": "certain"
9786
9786
  },
@@ -17731,6 +17731,15 @@
17731
17731
  "origin": "html-attr:placeholder",
17732
17732
  "confidence": "certain"
17733
17733
  },
17734
+ {
17735
+ "id": "299dd9d33f53",
17736
+ "surface": "builders-hall",
17737
+ "text": "restart it",
17738
+ "file": "modules/hall-ui/public/modules.js",
17739
+ "line": 129,
17740
+ "origin": "js-markup",
17741
+ "confidence": "certain"
17742
+ },
17734
17743
  {
17735
17744
  "id": "58b4e7a48d86",
17736
17745
  "surface": "builders-hall",
@@ -17943,7 +17952,7 @@
17943
17952
  "surface": "builders-hall",
17944
17953
  "text": "submit for review",
17945
17954
  "file": "modules/hall-ui/public/modules.js",
17946
- "line": 149,
17955
+ "line": 175,
17947
17956
  "origin": "js-markup",
17948
17957
  "confidence": "certain"
17949
17958
  },
@@ -18550,6 +18559,15 @@
18550
18559
  "origin": "js-markup",
18551
18560
  "confidence": "certain"
18552
18561
  },
18562
+ {
18563
+ "id": "354861d3c2ce",
18564
+ "surface": "builders-hall",
18565
+ "text": "waits for a restart",
18566
+ "file": "modules/hall-ui/public/modules.js",
18567
+ "line": 127,
18568
+ "origin": "js-markup",
18569
+ "confidence": "certain"
18570
+ },
18553
18571
  {
18554
18572
  "id": "35be9e0923da",
18555
18573
  "surface": "builders-hall",
@@ -18861,7 +18879,7 @@
18861
18879
  "surface": "builders-hall",
18862
18880
  "text": "{…} and above can enable or disable a module",
18863
18881
  "file": "modules/hall-ui/public/modules.js",
18864
- "line": 147,
18882
+ "line": 173,
18865
18883
  "origin": "js-markup",
18866
18884
  "confidence": "certain"
18867
18885
  },
@@ -19203,7 +19221,7 @@
19203
19221
  "surface": "builders-hall",
19204
19222
  "text": "{…} {…} — how-to",
19205
19223
  "file": "modules/hall-ui/public/modules.js",
19206
- "line": 390,
19224
+ "line": 416,
19207
19225
  "origin": "js-text-assign",
19208
19226
  "confidence": "certain"
19209
19227
  },
@@ -19275,7 +19293,7 @@
19275
19293
  "surface": "builders-hall",
19276
19294
  "text": "{…} — how to use it",
19277
19295
  "file": "modules/hall-ui/public/modules.js",
19278
- "line": 392,
19296
+ "line": 418,
19279
19297
  "origin": "js-text-assign",
19280
19298
  "confidence": "certain"
19281
19299
  },
@@ -19410,7 +19428,7 @@
19410
19428
  "surface": "builders-hall",
19411
19429
  "text": "· version",
19412
19430
  "file": "modules/hall-ui/public/modules.js",
19413
- "line": 395,
19431
+ "line": 421,
19414
19432
  "origin": "js-markup",
19415
19433
  "confidence": "certain"
19416
19434
  },
@@ -3077,5 +3077,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3077
3077
  landed since 1.21.118 with no explicit bump. run 38574092141. (task 1002620)
3078
3078
  1.21.120 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3079
3079
  landed since 1.21.119 with no explicit bump. run 38597828843. (task 1002620)
3080
+ 1.21.121 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3081
+ landed since 1.21.120 with no explicit bump. run 38604777189. (task 1002620)
3080
3082
  ---------------------------------------------------------------------------
3081
3083
  ```
@@ -1875,7 +1875,7 @@
1875
1875
  "surface": "builders",
1876
1876
  "title": "Modules",
1877
1877
  "states_read": ["browse","search-empty","lens","details","out"],
1878
- "files_hash": "22c9622ad3cbe0da",
1878
+ "files_hash": "c1d5725a6bf0afe2",
1879
1879
  "reading_hash": "e37d27e205c7ba63",
1880
1880
  "counts": {"lines":48,"placed":19,"shared":8,"unplaced":21},
1881
1881
  "omitted": {"data":76},
@@ -1917,7 +1917,7 @@
1917
1917
  {"key":"L0034","section":"Module catalog","text":"{…} file{…}","placement":"placed","file":"modules/hall-ui/public/gate.js","line":151,"string_id":"3efa67dc6e07"},
1918
1918
  {"key":"L0035","section":"Module catalog","text":"sessions","placement":"placed","file":"modules/hall-ui/public/task.js","line":467,"string_id":"732b4c935e36"},
1919
1919
  {"key":"L0036","section":"Module catalog","text":"Built by this project","placement":"placed","file":"modules/hall-ui/public/modules.html","line":70,"string_id":"b51edc826cbe"},
1920
- {"key":"L0037","section":"Module catalog","text":"submit for review","placement":"placed","file":"modules/hall-ui/public/modules.js","line":149,"string_id":"0215b2cdca8c"},
1920
+ {"key":"L0037","section":"Module catalog","text":"submit for review","placement":"placed","file":"modules/hall-ui/public/modules.js","line":175,"string_id":"0215b2cdca8c"},
1921
1921
  {"key":"L0038","section":"Module catalog","text":"A module built here can be submitted upstream for review, so it can join the shared catalog with your name and this project credited.","placement":"placed","file":"modules/hall-ui/public/modules.html","line":74,"string_id":"2e17d7e236d9"},
1922
1922
  {"key":"L0039","section":"footer","text":"Sign out","placement":"shared","file":"modules/hall-ui/public/shell.js","line":520,"string_id":"1883ad7f07fc"},
1923
1923
  {"key":"L0040","section":"Module catalog","text":"Builders hall · this session","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":810,"string_id":"fd1903586e8e"},
@@ -5129,7 +5129,7 @@
5129
5129
  "surface": "landing",
5130
5130
  "title": "{{productName}} — projects",
5131
5131
  "states_read": ["out-map","in-map","in-map-projects","in-map-profile","in-map-kbd","in-map-search","in-map-mine-on","in-table","in-table-by-builders","in-mine","in-manage","in-manage-address","in-manage-planet","in-manage-card","in-manage-look","in-manage-kind","in-manage-invite","in-manage-invite-handoff","in-manage-invite-guild","in-manage-app-live","in-manage-app-building","in-manage-app-failed","in-manage-app-retry","in-manage-app-offer","in-manage-app-connect","in-manage-snag","in-manage-gone","in-manage-missing","out-manage","in-new","in-new-named","in-new-ready","in-new-name-prefilled","in-new-demo","in-new-demo-used","in-new-checks-filled","in-new-checks-entity","in-new-row-code","in-new-row-address","in-new-row-team","in-new-row-extras","in-new-row-rewards","in-new-row-app","in-new-row-run","in-new-row-ship","in-new-look","in-new-look-custom","in-new-create-blocked","in-new-done","in-new-resume-offer","out-new","out-map-projects","card","card-noart","card-self","in-manage-demo","in-mine-dns-pending","in-manage-dns-pending","in-mine-empty","card-floor","card-backdrop","in-new-done-live","setup-just-created","setup-waiting-on-github","setup-installing","setup-stopped","setup-all-done","setup-cloud-hop-open"],
5132
- "files_hash": "82d94c8932993dbd",
5132
+ "files_hash": "6c567b2c13fd0fc6",
5133
5133
  "reading_hash": "302909900601b80c",
5134
5134
  "counts": {"lines":825,"placed":246,"shared":21,"unplaced":558},
5135
5135
  "omitted": {"data":150},
@@ -113,6 +113,32 @@
113
113
  return '<span class="fact-pill fact-pill--warn" title="Taken out of the store. Your copy keeps working, but it will get no more updates.">delisted · no more updates</span>';
114
114
  }
115
115
 
116
+ // A toggle this server has not acted on yet (task 1004463): routes mount and pollers
117
+ // start once, at boot, so the change lands at the next restart.
118
+ // The words are the server's (restartNote in src/bongos/routes/modules.js): 'routes' are
119
+ // its pages, 'pollers' its background jobs, and only what actually waits is named.
120
+ function restartFact(m) {
121
+ const waits = Array.isArray(m.restart_pending) ? m.restart_pending : [];
122
+ if (!waits.length) return '';
123
+ const parts = m.enabled
124
+ ? [waits.includes('routes') && 'its pages do not open', waits.includes('pollers') && 'its background jobs do not run']
125
+ : [waits.includes('routes') && 'its pages still open', waits.includes('pollers') && 'its background jobs still run'];
126
+ const title = `It turns ${m.enabled ? 'on' : 'off'} at the next restart of this server. Until then ${parts.filter(Boolean).join(' and ')}.`;
127
+ const pill = `<span class="fact-pill fact-pill--warn" title="${escapeHtml(title)}">waits for a restart</span>`;
128
+ const door = restartDoor();
129
+ return door ? pill + `<a class="fact-pill" href="${escapeHtml(door)}">restart it</a>` : pill;
130
+ }
131
+ // Where this server's Restart button is: only the platform's own hall has one, on its
132
+ // project page on the hub (task 1004463). Elsewhere whoever runs the server restarts it.
133
+ function restartDoor() {
134
+ const hub = window.OTB && typeof window.OTB.hubOrigin === 'function' ? window.OTB.hubOrigin() : null;
135
+ if (!hub) return null;
136
+ let hubHost = '';
137
+ try { hubHost = new URL(hub).hostname; } catch (err) { return null; }
138
+ const here = String(window.location.hostname || '').replace(/^builders\./, '');
139
+ return here === hubHost ? `${hub}/projects?view=mine` : null;
140
+ }
141
+
116
142
  // The lines the row's details disclosure opens onto: the facts the payload
117
143
  // carries that the card never rendered — what the module contributes, the
118
144
  // core it needs, who maintains it and what it hands off to.
@@ -159,7 +185,7 @@
159
185
  <div class="ov-row__main">
160
186
  <span class="ov-row__title">${escapeHtml(m.title || m.key)}<span class="ov-mono">${escapeHtml(m.key)}</span></span>
161
187
  ${desc ? `<p class="ov-row__sub mod-row__desc">${escapeHtml(desc)}</p>` : ''}
162
- <div class="ov-row__facts">${provenancePill(m)}${attributionFacts(m)}${upkeepFact(m)}${howtoFact(m)}${delistedFact(m)}</div>
188
+ <div class="ov-row__facts">${provenancePill(m)}${attributionFacts(m)}${upkeepFact(m)}${howtoFact(m)}${delistedFact(m)}${restartFact(m)}</div>
163
189
  ${lines ? `<details class="mod-more"${openKeys.has(m.key) ? ' open' : ''}><summary aria-label="Details for ${escapeHtml(m.title || m.key)}"></summary>${lines}</details>` : ''}
164
190
  </div>
165
191
  <div class="ov-row__right mod-row__right">${rightHtml(m)}</div>
@@ -289,13 +315,13 @@
289
315
  const body = res.data || {};
290
316
  if (!res.ok) throw new Error(body.message || `enable → ${res.status}`);
291
317
  const m = allModules.find((x) => x.key === key);
292
- if (m) m.enabled = next;
318
+ if (m) { m.enabled = next; if (Array.isArray(body.restart_pending)) m.restart_pending = body.restart_pending; }
293
319
  btn.setAttribute('aria-pressed', next ? 'true' : 'false');
294
320
  btn.textContent = next ? 'enabled' : 'disabled';
295
321
  const figs = $('#mod-figs');
296
322
  if (figs) figs.innerHTML = figsHTML();
297
323
  renderRows();
298
- toast(body.note || (next ? `${key} enabled.` : `${key} disabled.`), { duration: 3200 });
324
+ toast(body.note || (next ? `${key} enabled.` : `${key} disabled.`), { duration: body.restart_pending && body.restart_pending.length ? 7000 : 3200 });
299
325
  } catch (err) {
300
326
  console.error('[modules] toggle failed', err);
301
327
  toast(err.message || 'Could not change that module.', { duration: 3200 });
@@ -123,9 +123,11 @@ const SETTINGS_APPLY_SHAPES = new Set(['co-tenant', 'cloud-host', 'control-plane
123
123
  // SETTINGS_APPLY_FROM: each action declaring its own gate is this module's
124
124
  // convention, and the two answer different questions.
125
125
  const RESTART_FROM = new Set(['active']);
126
- // Same reachability rule as the settings push (the runner can only restart what it runs),
127
- // minus 'control-plane': task 1004334 gave the self row a settings push, not a restart door.
128
- const RESTART_SHAPES = new Set(['co-tenant', 'cloud-host']);
126
+ // Same reachability rule as the settings push (the runner can only restart what it runs).
127
+ // 'control-plane' joined in task 1004463: a module toggled in its hall takes effect only at
128
+ // the next restart, the guard already allows 'restart' on the self row, and the owner's only
129
+ // other way was a shell on the box.
130
+ const RESTART_SHAPES = new Set(['co-tenant', 'cloud-host', 'control-plane']);
129
131
 
130
132
  // Per-project settings (task 1003040, goal 1000058): the known keys + their
131
133
  // vocabularies, enforced at the API layer — the jsonb `settings` column
@@ -8541,9 +8541,11 @@ summary{min-height:24px;padding:3px 0;}
8541
8541
  last_verified_up === false is the verdict; null means never probed and is
8542
8542
  deliberately NOT the same claim (publicInstance keeps the two apart), so a
8543
8543
  brand-new project never grows a Restart button it has no use for. A door on
8544
- every healthy card would only invite needless downtime. */
8544
+ every healthy card would only invite needless downtime. The ONE exception is the
8545
+ platform's own row (task 1004463): a module switched on or off in its hall waits for
8546
+ a restart, so its owner always has the door. */
8545
8547
  function canRestart(inst) {
8546
- return inst.status === 'active' && inst.last_verified_up === false && !inst.open_intent;
8548
+ return inst.status === 'active' && !inst.open_intent && (inst.last_verified_up === false || inst.hosting_shape === 'control-plane');
8547
8549
  }
8548
8550
  /* keep watching while the platform is doing something to this instance */
8549
8551
  function shouldWatch(inst) {
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.120",
3
+ "version": "1.21.121",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.120",
9
+ "version": "1.21.121",
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.21.120",
3
+ "version": "1.21.121",
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",
@@ -9702,5 +9702,11 @@
9702
9702
  "id": "1004410",
9703
9703
  "text": "The new-project page no longer says GitHub sign-in is unfinished when it already works: the platform now checks the project itself and ticks the step."
9704
9704
  }
9705
+ ],
9706
+ "1.21.121": [
9707
+ {
9708
+ "id": "1004463",
9709
+ "text": "Turning a module on or off now says honestly when it takes effect at the next restart, and the platform's own server can be restarted from its project page instead of needing a login to the machine."
9710
+ }
9705
9711
  ]
9706
9712
  }
@@ -91,7 +91,7 @@ const EXEMPTIONS = [
91
91
  + 're-opens for review.',
92
92
  },
93
93
  {
94
- at: 'src/module-loader/loader.js:196',
94
+ at: 'src/module-loader/loader.js:198',
95
95
  why: 'The plugin loader, and the second site the audit named (B50 "motivating sites"). It '
96
96
  + 'require()s a discovered module\'s route file, so the specifier is a runtime path BY '
97
97
  + 'DESIGN — a plugin system whose module paths are compile-time constants is not a plugin '
@@ -103,7 +103,7 @@ const EXEMPTIONS = [
103
103
  + 'absorbed into this check.',
104
104
  },
105
105
  {
106
- at: 'src/module-loader/loader.js:309',
106
+ at: 'src/module-loader/loader.js:347',
107
107
  why: 'The poller twin of the route require at :196 — same DEFAULT_ROOTS bound, same plugin-by-'
108
108
  + 'design rationale, same ADR 0107 / task 1003400 follow-on. Listed on its own line so a '
109
109
  + 'change to either call re-opens only that one.',
@@ -927,8 +927,10 @@ async function settingsApplyInstance(inst, deps) {
927
927
  // ---------------------------------------------------------------------------
928
928
  async function restartInstance(inst, deps) {
929
929
  const { db, provisioning, exec, log, apply, privileged } = deps;
930
- // Same box routing as settingsApplyInstance: a standalone runs control-plane-local.
931
- const boxExec = inst.hosting_shape === 'cloud-host' ? (deps.controlExec || exec) : exec;
930
+ // Same box routing as settingsApplyInstance: a standalone runs control-plane-local, and the
931
+ // control plane's own row IS this box (task 1004463).
932
+ const onControlBox = inst.hosting_shape === 'cloud-host' || inst.hosting_shape === 'control-plane';
933
+ const boxExec = onControlBox ? (deps.controlExec || exec) : exec;
932
934
  const sudoP = privileged ? 'sudo ' : '';
933
935
  log(`restart ${inst.slug} (shape=${inst.hosting_shape}, status=${inst.status})`);
934
936
 
@@ -77,6 +77,8 @@ const express = require('express');
77
77
  const auth = require('../auth');
78
78
  const { asyncHandler, validateOrRespond, LIMITS } = require('./_helpers');
79
79
  const loader = require('../../module-loader/loader');
80
+ // named, not loader.restartPendingFor: knip sees a named require, never a member read
81
+ const { restartPendingFor } = require('../../module-loader/loader');
80
82
  const catalog = require('../../module-loader/catalog');
81
83
  const modulesLib = require('../../modules');
82
84
  // scripts/gds/ is a sibling CORE-package dir (both resolve under
@@ -110,6 +112,15 @@ const log = require('../logger').logger.child({ src: 'modules' });
110
112
  const { listOverrides, setOverride } = require('../module-overrides');
111
113
  const { listCatalog } = require('../module-listing');
112
114
 
115
+ // The sentence for a toggle that waits on a restart (task 1004463). 'routes' are the
116
+ // module's pages, 'pollers' its background jobs. PURE.
117
+ function restartNote(waits, enabled) {
118
+ const parts = enabled
119
+ ? [waits.includes('routes') && 'its pages do not open', waits.includes('pollers') && 'its background jobs do not run']
120
+ : [waits.includes('routes') && 'its pages still open', waits.includes('pollers') && 'its background jobs still run'];
121
+ return `It turns ${enabled ? 'on' : 'off'} at the next restart of this server. Until then ${parts.filter(Boolean).join(' and ')}.`;
122
+ }
123
+
113
124
  // A packaged submission embeds every file's content (base64) — cap the total
114
125
  // so a runaway/misbuilt module can't bloat the review-queue table. Generous
115
126
  // headroom over what a real feature module carries (routes + migrations + UI
@@ -258,6 +269,8 @@ module.exports = function buildModulesRouter() {
258
269
  ? `/builders/modules?howto=${encodeURIComponent(m.key)}&version=${encodeURIComponent(String(m.version))}`
259
270
  : null;
260
271
  m.store_delisted = m.source === 'instance' && delistedKeys.has(m.key);
272
+ // what of this module still waits for a restart to match its toggle (task 1004463)
273
+ m.restart_pending = restartPendingFor(m.key, m.enabled === true);
261
274
  }
262
275
  res.json({
263
276
  modules: all,
@@ -297,6 +310,9 @@ module.exports = function buildModulesRouter() {
297
310
  // the write path deliberately is not.
298
311
  const resolvedSet = await setOverride(key, enabled, { builderId: req.builder && req.builder.id });
299
312
  const effective = resolvedSet ? resolvedSet[key] === true : enabled;
313
+ // Routes mount and pollers start once, at boot (task 1004463): say so rather than
314
+ // "live now" when this change waits for the next restart of this server.
315
+ const waits = restartPendingFor(key, effective);
300
316
 
301
317
  console.log(`[gds] module "${key}" ${enabled ? 'enabled' : 'disabled'} by builder ${req.builder && req.builder.id}`);
302
318
  res.json({
@@ -308,9 +324,12 @@ module.exports = function buildModulesRouter() {
308
324
  // who is not told so will re-click the toggle and blame the feature.
309
325
  effective,
310
326
  source: (resolvedSet && resolvedSet.sources && resolvedSet.sources[key]) || 'override',
311
- note: effective === enabled
312
- ? 'Live on this box now, and durable — it survives the next deploy.'
313
- : `Recorded, but "${key}" still resolves to ${effective}: a higher-precedence layer (env, or the spend guard) is winning.`,
327
+ restart_pending: waits,
328
+ note: effective !== enabled
329
+ ? `Recorded, but "${key}" still resolves to ${effective}: a higher-precedence layer (env, or the spend guard) is winning.`
330
+ : waits.length
331
+ ? `Saved, and it survives the next deploy. ${restartNote(waits, enabled)}`
332
+ : 'Live on this box now, and durable — it survives the next deploy.',
314
333
  });
315
334
  }));
316
335
 
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.120'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.121'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -112,7 +112,7 @@ function discoverModules({ roots = DEFAULT_ROOTS, core = coreVersion() } = {}) {
112
112
  // Cached singleton for app code (lazy, like src/modules.js's modules()).
113
113
  let _cache = null;
114
114
  function discovered(opts) { return (_cache ||= discoverModules(opts)); }
115
- function _reset() { _cache = null; _startedPollers = []; }
115
+ function _reset() { _cache = null; _startedPollers = []; _boot = { routes: null, pollers: null }; _bootShape = null; }
116
116
 
117
117
  // The discovered manifests as MODULE_REGISTRY-shaped entries, so src/modules.js
118
118
  // can merge them with its built-in registry. Empty until a module exists.
@@ -190,8 +190,10 @@ function mountModuleRoutes(router, { isEnabled } = {}, opts) {
190
190
  const enabled = enabledModules({ isEnabled }, opts);
191
191
  verifyEnabledPorts(enabled);
192
192
  let mounted = 0;
193
- for (const { dir, manifest } of enabled) {
193
+ _boot.routes = new Set();
194
+ for (const { key, dir, manifest } of enabled) {
194
195
  const routeKeys = (manifest.contributes && manifest.contributes.routes) || [];
196
+ if (routeKeys.length) _boot.routes.add(key);
195
197
  for (const rk of routeKeys) {
196
198
  const factory = require(path.join(dir, 'routes', `${rk}.js`));
197
199
  const sub = typeof factory === 'function' ? factory() : factory;
@@ -284,6 +286,41 @@ function moduleWebSurfaces({ isEnabled } = {}, opts) {
284
286
  return out;
285
287
  }
286
288
 
289
+ // What this process mounted at boot (task 1004463): the keys whose routes were mounted
290
+ // and whose pollers started. null until that boot step has run in this process — so a
291
+ // script or test that never booted is never told a restart is pending.
292
+ let _boot = { routes: null, pollers: null };
293
+ // key → { routes, pollers }: whether each discovered module mounts routes / runs a poller in
294
+ // this process. Module files do not change under a running process, so it is worked out
295
+ // once, on first ask, rather than stat'ing disk on every GET /modules.
296
+ let _bootShape = null;
297
+ function bootShape(opts) {
298
+ if (_bootShape) return _bootShape;
299
+ _bootShape = new Map();
300
+ for (const { key, dir, manifest } of discovered(opts).loaded) {
301
+ const c = manifest.contributes || {};
302
+ _bootShape.set(key, {
303
+ routes: (c.routes || []).length > 0,
304
+ // the same rule startModulePollers applies: a poller with no file here runs elsewhere
305
+ pollers: (c.pollers || []).some((pk) => fs.existsSync(path.join(dir, 'pollers', `${pk}.js`))),
306
+ });
307
+ }
308
+ return _bootShape;
309
+ }
310
+
311
+ // Does turning module `key` to `enabled` wait for a restart in THIS process (task
312
+ // 1004463)? Routes mount and pollers start once, at boot, so a module switched on after
313
+ // boot has no pages yet, and one switched off keeps answering until the restart. Returns
314
+ // what waits — [] when the change is live now. PURE over the boot record and the manifest.
315
+ function restartPendingFor(key, enabled, opts) {
316
+ const shape = bootShape(opts).get(key);
317
+ if (!shape) return [];
318
+ const waits = [];
319
+ if (_boot.routes && shape.routes && _boot.routes.has(key) !== !!enabled) waits.push('routes');
320
+ if (_boot.pollers && shape.pollers && _boot.pollers.has(key) !== !!enabled) waits.push('pollers');
321
+ return waits;
322
+ }
323
+
287
324
  // Started poller handles (key, name, module), so stopModulePollers stops exactly
288
325
  // what startModulePollers started — and a test can introspect them.
289
326
  let _startedPollers = [];
@@ -299,6 +336,7 @@ let _startedPollers = [];
299
336
  // No-op when nothing is discovered/enabled. Returns the started-handle list.
300
337
  function startModulePollers(deps = {}, { isEnabled } = {}, opts) {
301
338
  const started = [];
339
+ _boot.pollers = new Set();
302
340
  for (const { key, dir, manifest } of discovered(opts).loaded) {
303
341
  if (typeof isEnabled === 'function' && !isEnabled(key)) continue;
304
342
  const pollerKeys = (manifest.contributes && manifest.contributes.pollers) || [];
@@ -323,6 +361,7 @@ function startModulePollers(deps = {}, { isEnabled } = {}, opts) {
323
361
  r.catch((e) => console.error(`[module-loader] poller ${key}/${pk} start error: ${e && e.message ? e.message : e}`));
324
362
  }
325
363
  started.push({ key, poller: pk, handle: poller });
364
+ _boot.pollers.add(key);
326
365
  }
327
366
  } catch (e) {
328
367
  console.error(`[module-loader] poller ${key}/${pk} start threw: ${e && e.message ? e.message : e}`);
@@ -370,6 +409,7 @@ module.exports = {
370
409
  moduleWebSurfaces,
371
410
  startModulePollers,
372
411
  stopModulePollers,
412
+ restartPendingFor,
373
413
  loaderSnapshot,
374
414
  coreVersion,
375
415
  dedupeRoots,
@@ -58,7 +58,7 @@ test('modules: the row is the oversight row — title with the mono key, clamped
58
58
  assert.match(js, /<span class="ov-row__title">\$\{escapeHtml\(m\.title \|\| m\.key\)\}<span class="ov-mono">\$\{escapeHtml\(m\.key\)\}<\/span>/, 'the key rides inside the title in mono');
59
59
  assert.match(js, /class="ov-row__sub mod-row__desc"/, 'the description is the sub line');
60
60
  assert.match(js, /<details class="mod-more"/, 'a details disclosure opens the rest');
61
- assert.match(js, /<div class="ov-row__facts">\$\{provenancePill\(m\)\}\$\{attributionFacts\(m\)\}\$\{upkeepFact\(m\)\}\$\{howtoFact\(m\)\}\$\{delistedFact\(m\)\}<\/div>/, 'provenance, credit, upkeep, (for a store version, task 1004366) the how-to link and (task 1003797) the delisted pill are the facts line');
61
+ assert.match(js, /<div class="ov-row__facts">\$\{provenancePill\(m\)\}\$\{attributionFacts\(m\)\}\$\{upkeepFact\(m\)\}\$\{howtoFact\(m\)\}\$\{delistedFact\(m\)\}\$\{restartFact\(m\)\}<\/div>/, 'provenance, credit, upkeep, (for a store version, task 1004366) the how-to link and (task 1003797) the delisted pill and (task 1004463) the waits-for-a-restart pill are the facts line');
62
62
  assert.match(js, /<div class="ov-row__right mod-row__right">\$\{rightHtml\(m\)\}<\/div>/, 'the right column is one function');
63
63
  const css = read('modules.css');
64
64
  assert.match(css, /\.mod-row__desc \{[^}]*-webkit-line-clamp: 2/, 'the description clamps to two lines');
@@ -462,3 +462,34 @@ test('the demo proof module mounts + serves through the loader (BV1.R52 / task 1
462
462
  assert.deepEqual(routes.sort(), ['GET /demo/health', 'POST /demo/ping']);
463
463
  loader._reset();
464
464
  });
465
+
466
+ // ---------------------------------------------------------------------------
467
+ // task 1004463 — a toggle says whether it waits for a restart. Routes mount and pollers
468
+ // start once, at boot, so restartPendingFor compares the toggle with what THIS process
469
+ // mounted; the Modules toggle used to claim "Live on this box now" regardless.
470
+ // ---------------------------------------------------------------------------
471
+
472
+ test('task 1004463: restartPendingFor names what waits for a restart, and nothing before boot', () => {
473
+ loader._reset();
474
+ const factory = "module.exports = () => function moduleRoute(req, res, next) { next(); };";
475
+ const poller = 'module.exports = { start() {}, stop() {} };';
476
+ const root = fixtureRoot({
477
+ pages: { manifest: validManifest('pages', { contributes: { routes: ['main'] } }), files: { 'routes/main.js': factory } },
478
+ jobs: { manifest: validManifest('jobs', { contributes: { routes: ['main'], pollers: ['tick'] } }), files: { 'routes/main.js': factory, 'pollers/tick.js': poller } },
479
+ remote: { manifest: validManifest('remote', { contributes: { pollers: ['elsewhere'] } }) },
480
+ quiet: { manifest: validManifest('quiet') },
481
+ });
482
+ const opts = { roots: [root], core: '1.0.0' };
483
+ assert.deepEqual(loader.restartPendingFor('pages', true, opts), [], 'a process that never booted is never told a restart is pending');
484
+ const bootOn = (k) => k === 'jobs';
485
+ loader.mountModuleRoutes({ use: () => {} }, { isEnabled: bootOn }, opts);
486
+ loader.startModulePollers({}, { isEnabled: bootOn }, opts);
487
+ assert.deepEqual(loader.restartPendingFor('pages', true, opts), ['routes'], 'switched on after boot: its pages wait');
488
+ assert.deepEqual(loader.restartPendingFor('pages', false, opts), [], 'back off: matches what is mounted');
489
+ assert.deepEqual(loader.restartPendingFor('jobs', false, opts), ['routes', 'pollers'], 'switched off after boot: pages and jobs keep running');
490
+ assert.deepEqual(loader.restartPendingFor('jobs', true, opts), []);
491
+ assert.deepEqual(loader.restartPendingFor('remote', true, opts), [], 'a poller with no file here never runs in this process');
492
+ assert.deepEqual(loader.restartPendingFor('quiet', true, opts), [], 'nothing mounted at boot, nothing to wait for');
493
+ assert.deepEqual(loader.restartPendingFor('nope', true, opts), []);
494
+ loader._reset();
495
+ });
@@ -0,0 +1,73 @@
1
+ // tests/module_toggle_restart.mjs — task 1004463.
2
+ //
3
+ // Measured on cloudbongos.com: switching a module on stored the override and the toggle
4
+ // said "Live on this box now", but the module's pages stayed 404 until the next restart,
5
+ // because routes mount (and pollers start) once, at boot. The toggle now asks the loader
6
+ // what still waits (restartPendingFor) and says so; GET /modules carries the same field
7
+ // so the hall can show "waits for a restart" on the row.
8
+ //
9
+ // Hermetic: setOverride is stubbed BEFORE the route file binds it, and the loader's
10
+ // answer is stubbed on the loader object the route calls through. The loader's own rule
11
+ // is tests/module_loader.mjs (task 1004463 case).
12
+ //
13
+ // Run: node --test tests/module_toggle_restart.mjs
14
+
15
+ import { strict as assert } from 'node:assert';
16
+ import { test } from 'node:test';
17
+ import { createRequire } from 'node:module';
18
+
19
+ const require = createRequire(import.meta.url);
20
+ const overrides = require('../src/bongos/module-overrides.js');
21
+ overrides.setOverride = async (key, enabled) => ({ [key]: enabled, sources: { [key]: 'override' } });
22
+ const loader = require('../src/module-loader/loader.js');
23
+ // the route binds restartPendingFor at require time, so stub it BEFORE the route loads and
24
+ // let each case choose the answer through `pending`
25
+ let pending = () => [];
26
+ loader.restartPendingFor = (...a) => pending(...a);
27
+ const buildModulesRouter = require('../src/bongos/routes/modules.js');
28
+
29
+ function handlerFor(router, method, path) {
30
+ const layer = router.stack.find((l) => l.route && l.route.path === path && l.route.methods[method]);
31
+ assert.ok(layer, `no ${method.toUpperCase()} ${path} on the router`);
32
+ return layer.route.stack[layer.route.stack.length - 1].handle;
33
+ }
34
+ async function invoke(handler, req) {
35
+ const res = {
36
+ statusCode: 200, body: undefined,
37
+ json(b) { this.body = b; return this; },
38
+ status(c) { this.statusCode = c; return this; },
39
+ fail(code, opts) { this.body = { error: code, ...opts }; return this; },
40
+ };
41
+ let err = null;
42
+ await handler(req, res, (e) => { err = e; });
43
+ if (err) throw err;
44
+ return res;
45
+ }
46
+ const router = buildModulesRouter();
47
+ const key = buildModulesRouter.listModules()[0].key;
48
+ const toggle = (enabled) => invoke(handlerFor(router, 'post', '/modules/:key/enable'), { params: { key }, query: {}, body: { enabled }, builder: { id: 1 } });
49
+
50
+ test('a toggle that waits for a restart says so, instead of "live now"', async () => {
51
+ pending = () => ['routes', 'pollers'];
52
+ const on = await toggle(true);
53
+ assert.deepEqual(on.body.restart_pending, ['routes', 'pollers']);
54
+ assert.doesNotMatch(on.body.note, /Live on this box now/);
55
+ assert.equal(on.body.note, 'Saved, and it survives the next deploy. It turns on at the next restart of this server. Until then its pages do not open and its background jobs do not run.');
56
+ pending = () => ['routes'];
57
+ const off = await toggle(false);
58
+ assert.equal(off.body.note, 'Saved, and it survives the next deploy. It turns off at the next restart of this server. Until then its pages still open.');
59
+ });
60
+
61
+ test('a toggle with nothing waiting is still live now', async () => {
62
+ pending = () => [];
63
+ const r = await toggle(true);
64
+ assert.deepEqual(r.body.restart_pending, []);
65
+ assert.equal(r.body.note, 'Live on this box now, and durable — it survives the next deploy.');
66
+ });
67
+
68
+ test('GET /modules carries restart_pending on every row', async () => {
69
+ pending = (k) => (k === key ? ['routes'] : []);
70
+ const r = await invoke(handlerFor(router, 'get', '/modules'), { params: {}, query: {}, body: {} });
71
+ for (const m of r.body.modules) assert.ok(Array.isArray(m.restart_pending), `${m.key} carries restart_pending`);
72
+ assert.deepEqual(r.body.modules.find((m) => m.key === key).restart_pending, ['routes']);
73
+ });
@@ -86,7 +86,10 @@ test('the restart door appears ONLY where it has a reason to exist, and mirrors
86
86
  // task 1004182 folded into 'cloud-host'. A `!== 'dedicated'` test against a value no row
87
87
  // can hold is always true, so keeping it would have been a condition that reads like a
88
88
  // guard and guards nothing.
89
- assert.doesNotMatch(body, /hosting_shape/, 'no shape term survives — the one it excluded no longer exists');
89
+ // task 1004463: the ONE shape term is an inclusion — the platform's own row always has the
90
+ // door, because a module toggled in its hall waits for a restart
91
+ assert.match(body, /inst.last_verified_up === false || inst.hosting_shape === 'control-plane'/, 'the self row is the one healthy card with a door');
92
+ assert.doesNotMatch(body, /dedicated/, 'the shape it once excluded no longer exists');
90
93
  assert.match(body, /!inst\.open_intent/, 'the strip tells the story while work is in flight');
91
94
  });
92
95
 
@@ -167,9 +167,9 @@ await ta('dry-run runs no real work and observes nothing', async () => {
167
167
  assert.equal(writes.length, 0);
168
168
  });
169
169
 
170
- await ta('a co-tenant restarts on the co-hosting box; a cloud-host on the control plane', async () => {
170
+ await ta("a co-tenant restarts on the co-hosting box; a cloud-host and the platform's own row on the control plane", async () => {
171
171
  const seen = {};
172
- for (const shape of ['co-tenant', 'cloud-host']) {
172
+ for (const shape of ['co-tenant', 'cloud-host', 'control-plane']) {
173
173
  const box = [], control = [];
174
174
  const { deps } = recorder();
175
175
  deps.exec = (cmd) => { box.push(cmd); return /healthz/.test(cmd) ? { ok: true, stdout: '{}' } : { ok: true, stdout: '' }; };
@@ -179,6 +179,7 @@ await ta('a co-tenant restarts on the co-hosting box; a cloud-host on the contro
179
179
  }
180
180
  assert.ok(seen['co-tenant'].box > 0 && seen['co-tenant'].control === 0, 'a co-tenant runs on the box');
181
181
  assert.ok(seen['cloud-host'].control > 0 && seen['cloud-host'].box === 0, 'a cloud-host runs control-plane-local (task 2074) — restart it THERE');
182
+ assert.ok(seen['control-plane'].control > 0 && seen['control-plane'].box === 0, 'the self row IS this box (task 1004463)');
182
183
  });
183
184
 
184
185
  await ta('privileged mode escalates the restart', async () => {
@@ -89,14 +89,17 @@ t('RESTART_FROM is exactly {active} — every other state is already served by t
89
89
  }
90
90
  });
91
91
 
92
- t('RESTART_SHAPES is the runner-reachable pair, and the set is a subset of HOSTING_SHAPES', () => {
93
- assert.deepEqual([...provisioning.RESTART_SHAPES].sort(), ['cloud-host', 'co-tenant']);
92
+ t('RESTART_SHAPES is what the runner can reach, and the set is a subset of HOSTING_SHAPES', () => {
93
+ // task 1004463 added the control plane's own row: its hall's module toggles wait for a restart
94
+ assert.deepEqual([...provisioning.RESTART_SHAPES].sort(), ['cloud-host', 'co-tenant', 'control-plane']);
95
+ const G = require('../modules/provisioning/control-plane-guard.js');
96
+ assert.equal(G.intentAllowedForShape('restart', 'control-plane'), true, 'the self-row guard lets the restart through, so the route never promises one it refuses');
94
97
  for (const s of provisioning.RESTART_SHAPES) assert.ok(provisioning.HOSTING_SHAPES.includes(s), `${s} is a real shape`);
95
98
  // Was `!has('dedicated')`. Task 1004182 removed that value from HOSTING_SHAPES, which made
96
99
  // the assertion vacuously true — it would now pass against any set at all. Name the shapes
97
100
  // that still EXIST and are still unreachable, and assert they are real first, so this
98
101
  // cannot rot the same way twice.
99
- for (const unreachable of ['byo-host', 'control-plane']) {
102
+ for (const unreachable of ['byo-host']) {
100
103
  assert.ok(provisioning.HOSTING_SHAPES.includes(unreachable), `${unreachable} must be a real shape or this assertion is vacuous`);
101
104
  assert.ok(!provisioning.RESTART_SHAPES.has(unreachable), unreachable);
102
105
  }
@@ -92,8 +92,8 @@ t('the two gates agree: every settings-apply shape is one the self-row guard let
92
92
  // "a restart is coming" and then answer the owner with control_plane_protected.
93
93
  const G = require('../modules/provisioning/control-plane-guard.js');
94
94
  for (const s of provisioning.SETTINGS_APPLY_SHAPES) assert.equal(G.intentAllowedForShape('settings-apply', s), true, s);
95
- // and the restart door was NOT opened on the self row as a side effect
96
- assert.ok(!provisioning.RESTART_SHAPES.has('control-plane'), 'task 1004334 widened the settings push only');
95
+ // the restart door on the self row came later, deliberately (task 1004463)
96
+ assert.ok(provisioning.RESTART_SHAPES.has('control-plane'), 'task 1004463 opened the restart door on the self row');
97
97
  });
98
98
 
99
99
  await ta('enqueueSettingsApply: a fresh intent → created:true; the INSERT carries the settings-apply action', async () => {