@bongos/core 1.19.872 → 1.19.874

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.872",
6
- "core_contract": "1.19.872",
7
- "source_commit": "847fc8087c9a995a4d3cd60606d594d66e209773",
5
+ "core_version": "1.19.874",
6
+ "core_contract": "1.19.874",
7
+ "source_commit": "57108fe11dd040dbf550355c6ff492141c283504",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-19T03:43:12.261Z",
9
+ "built_at": "2026-09-19T03:56:27.228Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 500,
13
13
  "agent_docs_stubbed": 26,
14
- "functional_verbatim": 2300,
14
+ "functional_verbatim": 2301,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2826,
20
- "tree_sha256": "5535807e0c119bfd1e626826227726d034838568c556eb76fe7d367566a79155",
19
+ "file_count": 2827,
20
+ "tree_sha256": "28457c8c4f76254953c87b849337aa0804581b4ee7e2c2a696b93afb5338b8dc",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -1382,7 +1382,7 @@
1382
1382
  {
1383
1383
  "path": "docs/adr/0180-dependency-intake-and-drift-policy.md",
1384
1384
  "mode": "0000644",
1385
- "sha256": "9d50f2f71de5c4eba8d5af40aca201c6e054b1eec937a9e1342d1bb3b4ce45c1"
1385
+ "sha256": "d302683d028895648062a8128815c9a54db95ef174eee14f06af676587a380e5"
1386
1386
  },
1387
1387
  {
1388
1388
  "path": "docs/adr/0181-teardown-state-gate-and-address-retention.md",
@@ -2072,12 +2072,12 @@
2072
2072
  {
2073
2073
  "path": "docs/copy-inventory.md",
2074
2074
  "mode": "0000644",
2075
- "sha256": "c6ed9bece0a261ff37a10f36ac6179566d0ee757c95bb68e2a19bf3c0f02b300"
2075
+ "sha256": "4671a9e1ebce9a803afcd531d5da4ce939a2b228c62c916100c6748b37f6e72e"
2076
2076
  },
2077
2077
  {
2078
2078
  "path": "docs/copy-registry.json",
2079
2079
  "mode": "0000644",
2080
- "sha256": "e06a5c2011c25018fcceb437fdfc5c61a1dd7cd7415d5096ad1da58da20cc8d2"
2080
+ "sha256": "33e3a3376c0d444bb20d0b0f8ae7fc4808539963e7adf17d614ded6fcbd3a1d3"
2081
2081
  },
2082
2082
  {
2083
2083
  "path": "docs/design/apex-pass-2-direction.md",
@@ -2602,7 +2602,7 @@
2602
2602
  {
2603
2603
  "path": "docs/module-api-changelog.md",
2604
2604
  "mode": "0000644",
2605
- "sha256": "a581f0b40b8f6ba0fe33d5ae858c3c4048ba0bdec90f212d4e28de1191533565"
2605
+ "sha256": "688d9a2c95e934fca0e1df1a685f0cc72844cbe98b718e40f8ae7c49e3f65c9e"
2606
2606
  },
2607
2607
  {
2608
2608
  "path": "docs/modules-contract.md",
@@ -2812,7 +2812,7 @@
2812
2812
  {
2813
2813
  "path": "docs/recipes/private-npm-distribution.md",
2814
2814
  "mode": "0000644",
2815
- "sha256": "6d488eff1dd8201debb991ba854c59b5404235b1a433403dbedecaf0638128e4"
2815
+ "sha256": "5e4213d19d7f13ff0a378f996c7e909e00cf262516e72f96e44d4ddf0d4cc861"
2816
2816
  },
2817
2817
  {
2818
2818
  "path": "docs/recipes/redteam-patrol.md",
@@ -4872,12 +4872,12 @@
4872
4872
  {
4873
4873
  "path": "modules/hall-ui/public/deploy.html",
4874
4874
  "mode": "0000644",
4875
- "sha256": "23e54a15a796a7f7176cea0fcfcc72f92e5a008c851de25e443b74a2af8951ed"
4875
+ "sha256": "1dabc13d4c3dc59e10918348e0607ab7162522ba91d0269e3d4db3fbf5f0001e"
4876
4876
  },
4877
4877
  {
4878
4878
  "path": "modules/hall-ui/public/deploy.js",
4879
4879
  "mode": "0000644",
4880
- "sha256": "a44f3c64894c8f51b50e28ad42c9e8e52d60eb1a1d6098cdaf514f16492ba0b2"
4880
+ "sha256": "6676d4b7f5eb4a55d7cc4070824fc174f0ce7a74ce25441834e1d6175367fadc"
4881
4881
  },
4882
4882
  {
4883
4883
  "path": "modules/hall-ui/public/diagram-viewer.css",
@@ -7932,12 +7932,12 @@
7932
7932
  {
7933
7933
  "path": "package-lock.json",
7934
7934
  "mode": "0000644",
7935
- "sha256": "05e6bfd775abf8fa8ae50d6f40600767660564b8a4873d52beea120773cf5d96"
7935
+ "sha256": "7ae11b1bc6205785da22165907051680ba4abe00c7536b9c668488bb0667b78b"
7936
7936
  },
7937
7937
  {
7938
7938
  "path": "package.json",
7939
7939
  "mode": "0000644",
7940
- "sha256": "bc23e285afda64a58c1480f713447d5d5b18d6aa6e0fc5ccd77d0f275e297103"
7940
+ "sha256": "ef1cb37663f2cde6765b0ec2dc9df5bdc85ae91bbc63abdebe7aa607d436a6d4"
7941
7941
  },
7942
7942
  {
7943
7943
  "path": "public-docs/index.html",
@@ -8892,7 +8892,7 @@
8892
8892
  {
8893
8893
  "path": "scripts/gds/provision-repo.js",
8894
8894
  "mode": "0000644",
8895
- "sha256": "b04300749c4f119912ecfb2e6df4a6703f004130570e47a4e5bfaffacd7e2118"
8895
+ "sha256": "2ce976a62d123a284172c054bbc378c33977d2efda44e4b94ef2751a2626944d"
8896
8896
  },
8897
8897
  {
8898
8898
  "path": "scripts/gds/provision-teardown.js",
@@ -9922,7 +9922,7 @@
9922
9922
  {
9923
9923
  "path": "src/module-api.js",
9924
9924
  "mode": "0000644",
9925
- "sha256": "cb2d09a82fc7149dafd2ec0934ae6304481ce2c14e56fa75f3516fab2d37d974"
9925
+ "sha256": "1f7faccdbfb12ca1f1802d7703733b8bf9dd7ff9c2d5fd0bf96ea4c897a5d162"
9926
9926
  },
9927
9927
  {
9928
9928
  "path": "src/module-loader/catalog.js",
@@ -10949,6 +10949,11 @@
10949
10949
  "mode": "0000644",
10950
10950
  "sha256": "1abfa6c8bb119d010d2a53f71373508a31a9cc3c28207ca1dbac2138a7eda595"
10951
10951
  },
10952
+ {
10953
+ "path": "tests/deploy_page_projects.mjs",
10954
+ "mode": "0000644",
10955
+ "sha256": "2de9feef1fa6be72e8f9866a28b262149628316042f277c20d1bc00a088ec5d4"
10956
+ },
10952
10957
  {
10953
10958
  "path": "tests/design_tokens_sync.mjs",
10954
10959
  "mode": "0000644",
@@ -12572,7 +12577,7 @@
12572
12577
  {
12573
12578
  "path": "tests/package_core.mjs",
12574
12579
  "mode": "0000644",
12575
- "sha256": "7d64e3c217274af48f24557d49deeb2cb54d7929bd9e939f15b3d3ceb757336c"
12580
+ "sha256": "dafc62752d62949ed465d102b1bc5598ca3d4210a6d5d01d0be8bcf93f8413fc"
12576
12581
  },
12577
12582
  {
12578
12583
  "path": "tests/parallel-load.mjs",
@@ -37,6 +37,12 @@ The publish-on-merge lane (ADR 0161) authenticates with a long-lived `NPM_TOKEN`
37
37
  > - **Provenance had to be turned OFF.** Trusted publishing enables it by default and npm *refuses* to generate provenance from a **private source repository** — so the "provenance for free" clause above does not apply to this repo, and leaving the default would have failed every publish.
38
38
  > - **The registry could no longer answer "is this version taken?".** `@bongos/core` is private, so an unauthenticated `npm view` is a flat `E404` rather than a "no", and OIDC authenticates `npm publish` and nothing else. Keeping that check would have skipped the auto-bump on every run and collided on the published version — **breaking after exactly one release**. The lane now reads its own `release: core X.Y.Z` commits (`laneReleasedVersions`), which need no auth and are the source the cascade terminator already trusts.
39
39
 
40
+ > **Closed 2026-09-18 — task 1003262. Migrating the lane is not the same as deleting the credential, and for three weeks only the first had happened.** `publish.yml` stopped using `NPM_TOKEN` on 2026-08-25 and the repository secret was removed, after which this ADR, [ADR 0161](0161-publish-on-merge.md) and the red-team follow-up all recorded that "no long-lived npm credential exists to steal". That was true of the *repo* and false of the *npm account*: the granular write token `ci-publish-on-merge` (scope `@bongos`, package:write, 2FA-bypass, expiring 2026-11-06) was never revoked — it simply stopped being used, which produces no signal at all. A credential nothing reads is not a credential nothing can use.
41
+ >
42
+ > The same shape a second time, for reads: `droplet-registry-read` outlived [ADR 0283](0283-the-core-publishes-publicly-because-access-was-never-the-thing-protecting-it.md)'s public flip and `fcea51ca`'s deletion of every code path that consumed it — and that one had already leaked, in cleartext, into the droplet's persistent journal (blocker 1000118).
43
+ >
44
+ > **The rule this adds: a credential is retired when it is revoked at the issuer, not when the last caller stops passing it.** Removing the use is the safe half and the reversible half; it is not the finish. Both tokens are enumerated in this task's ship notes, and `npm token list` is the check — the repo cannot answer this question about itself, which is why nothing caught it.
45
+
40
46
  ## What this deliberately does not cover
41
47
 
42
48
  New-dependency *intake vetting* (registry existence/age/download checks — the slopsquatting defense) lives in the weekly patrol (task 1003128), and one-off vulnerability *fixes* (sharp ≥ 0.35 for the libvips CVEs, the colyseus chain for nanoid) are ordinary tasks that shrink the baseline — filed in the idea inbox at adoption time.
@@ -348,6 +348,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
348
348
  | `2366356926b4` | certain | Could not mark the primer just now. Try again. | `modules/hall-ui/public/primer.js:260` |
349
349
  | `753987b5da13` | certain | Could not reach the submit endpoint. | `modules/hall-ui/public/modules.js:297` |
350
350
  | `6f500726f0ac` | certain | Could not read the report: {…} | `modules/hall-ui/public/watch.js:640` |
351
+ | `21c2f33168a3` | certain | Could not read this project's deploy state: {…} | `modules/hall-ui/public/deploy.js:157` |
351
352
  | `8b470cebf53d` | certain | Could not remove your key. Try again. | `modules/hall-ui/public/settings.js:1153` |
352
353
  | `4cf6bc8b377b` | certain | Could not remove. Try again. | `modules/hall-ui/public/settings.js:1152` |
353
354
  | `7a778d188e87` | certain | Could not remove: {…} | `modules/hall-ui/public/government.js:369` |
@@ -629,14 +630,13 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
629
630
  | `73677b6839d7` | certain | Looks good | `modules/hall-ui/public/studio.js:162` |
630
631
  | `a93ca2841b96` | certain | Mac (Intel) | `modules/hall-ui/public/hall-render.js:825` |
631
632
  | `06b4ede3fb58` | certain | Machine principals | `modules/hall-ui/public/government.html:122` |
632
- | `c4b39de1e478` | certain | Make it live | `modules/hall-ui/public/deploy.js:128` |
633
633
  | `6b3d660e0e31` | certain | Map view | `modules/hall-ui/public/atlas.html:46` |
634
634
  | `8bdec961e84a` | certain | Markets, incentives, and how to price them. | `modules/hall-ui/public/settings.html:308` |
635
635
  | `6e73716aec6c` | certain | May be offered for sale later | `modules/hall-ui/public/settings.html:324` |
636
636
  | `df335a1224ae` | certain | Member since {…} | `modules/hall-ui/public/hall-render.js:563` |
637
637
  | `df335a1224ae` | certain | Member since {…} | `modules/hall-ui/public/profile.js:94` |
638
638
  | `c7a670e9552d` | certain | Merged into | `modules/hall-ui/public/profile.js:406` |
639
- | `6bd456635167` | certain | Merging work does not put it in front of anyone. This is the step that does: it moves this instance onto a new | `modules/hall-ui/public/deploy.html:39` |
639
+ | `dce0546c9e34` | certain | Merging work does not put it in front of anyone. This is the step that does: it moves this platform — or any p | `modules/hall-ui/public/deploy.html:39` |
640
640
  | `7637613c231b` | certain | Metic rank and above may vote | `modules/hall-ui/public/builders.js:790` |
641
641
  | `91328a97fe68` | certain | Mingle pairs you with someone from another role. Saying no costs nothing. | `modules/hall-ui/public/collab.html:80` |
642
642
  | `d7ce4a3bb745` | certain | Minimize the welcome panel | `modules/hall-ui/public/hall-widgets.js:52` |
@@ -650,8 +650,8 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
650
650
  | `1ae18e58fead` | certain | Most recent first. Each load of this page is itself recorded in the audit log. | `modules/hall-ui/public/sessions.js:340` |
651
651
  | `f14a91b3f625` | certain | Most recent first. Each row is one recorded work session; every id on it opens its record. | `modules/hall-ui/public/sessions.html:56` |
652
652
  | `e52ae74f0052` | certain | Move to goal | `modules/hall-ui/public/task.js:577` |
653
- | `e77d5f3beaef` | certain | Move to {…} | `modules/hall-ui/public/deploy.js:103` |
654
- | `e77d5f3beaef` | certain | Move to {…} | `modules/hall-ui/public/deploy.js:161` |
653
+ | `e77d5f3beaef` | certain | Move to {…} | `modules/hall-ui/public/deploy.js:122` |
654
+ | `e77d5f3beaef` | certain | Move to {…} | `modules/hall-ui/public/deploy.js:217` |
655
655
  | `ae84ebe1d735` | certain | Name cannot be blank. | `modules/hall-ui/public/settings.js:763` |
656
656
  | `31e91ee6ce03` | certain | Name must be {…} characters or fewer. | `modules/hall-ui/public/settings.js:768` |
657
657
  | `5e8199266a8a` | certain | Name unvouched | `modules/hall-ui/public/watch.js:374` |
@@ -687,6 +687,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
687
687
  | `2a6d7552e21f` | certain | No other sessions. | `modules/hall-ui/public/settings-sessions.js:64` |
688
688
  | `9f73b3f3dad0` | certain | No rank floor — any builder can claim this task | `modules/hall-ui/public/task-lib.js:62` |
689
689
  | `3fd0c6fcb92e` | certain | No request and no invite needed. Sign in with GitHub, and a step-by-step guide takes you the rest of the way. | `modules/hall-ui/public/hall-render.js:277` |
690
+ | `2947c1ddea9a` | certain | No running box to deploy. | `modules/hall-ui/public/deploy.js:195` |
690
691
  | `9fb9f766450d` | certain | No sections yet | `modules/hall-ui/public/primer.js:131` |
691
692
  | `bef9fe43ba84` | certain | No sessions recorded yet. They land here as builders ship and end sessions. | `modules/hall-ui/public/sessions.js:146` |
692
693
  | `4485497d87bb` | certain | No specialities yet. Create one below — it takes a name and a craft, and you can train it afterwards. | `modules/hall-ui/public/settings-specialities.js:169` |
@@ -717,7 +718,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
717
718
  | `fec5d78497e8` | certain | Nothing in the backlog right now. | `modules/hall-ui/public/work.js:1105` |
718
719
  | `905e99f158c2` | certain | Nothing is discarded by omission: every task needs a decision before this can be submitted. Abandoning stamps | `modules/hall-ui/public/goals-render.js:1007` |
719
720
  | `2ec0fbb20a42` | certain | Nothing is waiting. A spark appears here when its author offers a quick idea up for development. | `modules/hall-ui/public/ideas.js:543` |
720
- | `9ac63d97239a` | certain | Nothing newer is published that this box can take. There is nothing to deploy. | `modules/hall-ui/public/deploy.js:126` |
721
+ | `9ac63d97239a` | certain | Nothing newer is published that this box can take. There is nothing to deploy. | `modules/hall-ui/public/deploy.js:140` |
721
722
  | `ad9a17a8a5eb` | certain | Nothing shipped yet. | `modules/hall-ui/public/profile.js:242` |
722
723
  | `ad9a17a8a5eb` | certain | Nothing shipped yet. | `modules/hall-ui/public/work.js:1066` |
723
724
  | `f326632f2013` | certain | Nothing to submit — every module here ships with the core. | `modules/hall-ui/public/modules.js:258` |
@@ -901,7 +902,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
901
902
  | `324af64c8b6e` | certain | See all {…} goals {…} | `modules/hall-ui/public/roadmap.js:214` |
902
903
  | `a9c398651862` | certain | See the cross-project record | `modules/hall-ui/public/profile-canonical.js:114` |
903
904
  | `17d5b5739f46` | certain | See the work board → | `modules/hall-ui/public/hall-render.js:1019` |
904
- | `55cde0774831` | certain | See what would change | `modules/hall-ui/public/deploy.js:91` |
905
+ | `55cde0774831` | certain | See what would change | `modules/hall-ui/public/deploy.js:108` |
905
906
  | `75dc5463facc` | certain | Select at least 2 waiting tasks to copy a parallel-session prompt. | `modules/hall-ui/public/work.js:816` |
906
907
  | `a98882faa482` | certain | Select at least 2 waiting tasks to copy a sequential prompt. | `modules/hall-ui/public/work.js:841` |
907
908
  | `582bdd9435eb` | certain | Select task {…} for bulk actions | `modules/hall-ui/public/work.js:452` |
@@ -909,7 +910,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
909
910
  | `7e1d54119e08` | certain | Select task {…} for bulk release | `modules/hall-ui/public/work.js:242` |
910
911
  | `ae43a132699b` | certain | Selectable today | `modules/hall-ui/public/government.js:799` |
911
912
  | `53cc6d8dd23c` | certain | Send invitation | `modules/hall-ui/public/goals-render.js:1206` |
912
- | `fcd68c6a21fd` | certain | Serving now | `modules/hall-ui/public/deploy.js:115` |
913
+ | `a39b0f9d86e5` | certain | Serving {…} — read from the box itself, {…}. | `modules/hall-ui/public/deploy.js:142` |
913
914
  | `64a1f055129e` | certain | Session card richness | `modules/hall-ui/public/settings-render.js:83` |
914
915
  | `d14e8e9acaff` | certain | Session ended. | `modules/hall-ui/public/settings-sessions.js:81` |
915
916
  | `55e0a34bafed` | certain | Session log{…} | `modules/hall-ui/public/task.js:487` |
@@ -1090,7 +1091,8 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1090
1091
  | `9a685b01e2c6` | certain | This page opens at a higher rank. | `modules/hall-ui/public/harbor.html:37` |
1091
1092
  | `9a685b01e2c6` | certain | This page opens at a higher rank. | `modules/hall-ui/public/project-settings.html:36` |
1092
1093
  | `9a685b01e2c6` | certain | This page opens at a higher rank. | `modules/hall-ui/public/watch.html:40` |
1093
- | `77e3b8f5318b` | certain | This restarts the platform onto core | `modules/hall-ui/public/deploy.js:103` |
1094
+ | `f93fcb24c6ba` | certain | This platform | `modules/hall-ui/public/deploy.js:187` |
1095
+ | `6e9dfe1fb903` | certain | This restarts {…} onto core | `modules/hall-ui/public/deploy.js:122` |
1094
1096
  | `69e8beced2ff` | certain | This sends the idea back to its author. Say why, and which section. | `modules/hall-ui/public/board-room.js:276` |
1095
1097
  | `09256febc32d` | certain | This sentence has {…} {…} where a value is dropped in when it is shown. | `modules/hall-ui/public/copy-desk.js:422` |
1096
1098
  | `c9e50a67988a` | certain | This task has shipped and its credits have landed. Neither Ship it nor Confirm is available. | `modules/hall-ui/public/task.js:715` |
@@ -1296,7 +1298,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1296
1298
  | `77ad318bb546` | certain | invited — awaiting their response | `modules/hall-ui/public/goals-render.js:1395` |
1297
1299
  | `af0777cd098b` | certain | is hardcoded back inside this | `modules/hall-ui/public/index.html:56` |
1298
1300
  | `c767b3e07826` | certain | is only the screen; the | `modules/hall-ui/public/diagrams.html:74` |
1299
- | `d0e722302840` | certain | is published and this box is not on it yet. | `modules/hall-ui/public/deploy.js:91` |
1301
+ | `d0e722302840` | certain | is published and this box is not on it yet. | `modules/hall-ui/public/deploy.js:108` |
1300
1302
  | `aa530bd15088` | certain | itself). The registry stays the single source of truth for what the hall shows; a fitness check (scripts/gds/f | `modules/hall-ui/public/index.html:53` |
1301
1303
  | `9018bf6e99eb` | certain | last used {…} · signed in {…} | `modules/hall-ui/public/settings-sessions.js:46` |
1302
1304
  | `2ce7738b1eb0` | certain | loading goals… | `modules/hall-ui/public/task-detail.js:482` |
@@ -1313,7 +1315,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1313
1315
  | `b9f2e0d43c94` | certain | no clock | `modules/hall-ui/public/government.js:607` |
1314
1316
  | `e3317d58ec83` | certain | no cost logged | `modules/hall-ui/public/hall-render.js:352` |
1315
1317
  | `e02979266baf` | certain | no data | `modules/hall-ui/public/hall-kit.js:735` |
1316
- | `9e194a57d054` | certain | no reading | `modules/hall-ui/public/deploy.js:74` |
1318
+ | `9e194a57d054` | certain | no reading | `modules/hall-ui/public/deploy.js:91` |
1317
1319
  | `33ca71bef112` | certain | no tasks yet | `modules/hall-ui/public/goals-render.js:163` |
1318
1320
  | `739a4bd6bd83` | certain | node scripts/setup-builder.js | `modules/hall-ui/public/hall-render.js:892` |
1319
1321
  | `61f0f4b2f4ab` | certain | not categorised yet | `modules/hall-ui/public/goals-page.js:277` |
@@ -1343,7 +1345,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1343
1345
  | `2fb1dad96280` | certain | pending, on the People tab | `modules/hall-ui/public/watch.html:46` |
1344
1346
  | `3b22c5af029b` | certain | rank or above, or that it is your own. | `modules/hall-ui/public/copy-desk.html:47` |
1345
1347
  | `80ddf28baee7` | certain | rank-key (kebab-case) | `modules/hall-ui/public/government.html:82` |
1346
- | `41a4b66c21e5` | certain | reads the control plane's own observation · ADR 0293 · every move is recorded with who asked | `modules/hall-ui/public/deploy.js:132` |
1348
+ | `41a4b66c21e5` | certain | reads the control plane's own observation · ADR 0293 · every move is recorded with who asked | `modules/hall-ui/public/deploy.js:199` |
1347
1349
  | `f83f0a9d6b6a` | certain | recorded, on the Operations tab | `modules/hall-ui/public/watch.html:49` |
1348
1350
  | `26da1df6fd4a` | certain | reflects it. | `modules/hall-ui/public/settings.html:136` |
1349
1351
  | `d4bb81532896` | certain | repro steps, context, anything that helps… | `modules/hall-ui/public/ideas.html:84` |
@@ -1445,9 +1447,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1445
1447
  | `a6e4ed98fbea` | certain | {…} · revoke it any time under Active sessions. | `modules/hall-ui/public/settings.js:942` |
1446
1448
  | `d73256fbc395` | certain | {…} · who votes, and how it {…} | `modules/hall-ui/public/board-room.js:357` |
1447
1449
  | `cab2a4296aec` | certain | {…} · {…} · {…}{…} | `modules/hall-ui/public/goal-inbox.js:326` |
1448
- | `d9b4ae2db0f4` | certain | {…} — read from the box itself, {…}. | `modules/hall-ui/public/deploy.js:115` |
1449
1450
  | `085f93f3f775` | certain | {…} — see all standings | `modules/hall-ui/public/hall-render.js:80` |
1450
- | `b814cb35ce2f` | certain | {…} — the box this platform runs on | `modules/hall-ui/public/deploy.js:168` |
1451
1451
  | `f75ee96a7511` | certain | {…} — total payout from shipped work. | `modules/hall-ui/public/profile.js:94` |
1452
1452
  | `2a2728a4ec4f` | certain | {…}% of tasks complete | `modules/hall-ui/public/roadmap.js:70` |
1453
1453
  | `15e545eb3ce2` | certain | {…}, no such task | `modules/hall-ui/public/task.js:959` |
@@ -1075,8 +1075,8 @@
1075
1075
  "text": "Move to {…}",
1076
1076
  "occurrences": 2,
1077
1077
  "at": [
1078
- "modules/hall-ui/public/deploy.js:103",
1079
- "modules/hall-ui/public/deploy.js:161"
1078
+ "modules/hall-ui/public/deploy.js:122",
1079
+ "modules/hall-ui/public/deploy.js:217"
1080
1080
  ]
1081
1081
  },
1082
1082
  {
@@ -4265,6 +4265,15 @@
4265
4265
  "origin": "js-markup",
4266
4266
  "confidence": "certain"
4267
4267
  },
4268
+ {
4269
+ "id": "21c2f33168a3",
4270
+ "surface": "builders-hall",
4271
+ "text": "Could not read this project's deploy state: {…}",
4272
+ "file": "modules/hall-ui/public/deploy.js",
4273
+ "line": 157,
4274
+ "origin": "js-markup",
4275
+ "confidence": "certain"
4276
+ },
4268
4277
  {
4269
4278
  "id": "8b470cebf53d",
4270
4279
  "surface": "builders-hall",
@@ -6794,15 +6803,6 @@
6794
6803
  "origin": "html-text",
6795
6804
  "confidence": "certain"
6796
6805
  },
6797
- {
6798
- "id": "c4b39de1e478",
6799
- "surface": "builders-hall",
6800
- "text": "Make it live",
6801
- "file": "modules/hall-ui/public/deploy.js",
6802
- "line": 128,
6803
- "origin": "js-markup",
6804
- "confidence": "certain"
6805
- },
6806
6806
  {
6807
6807
  "id": "6b3d660e0e31",
6808
6808
  "surface": "builders-hall",
@@ -6858,9 +6858,9 @@
6858
6858
  "confidence": "certain"
6859
6859
  },
6860
6860
  {
6861
- "id": "6bd456635167",
6861
+ "id": "dce0546c9e34",
6862
6862
  "surface": "builders-hall",
6863
- "text": "Merging work does not put it in front of anyone. This is the step that does: it moves this instance onto a newer core, waits for the box to answer, and puts itself back if it does not.",
6863
+ "text": "Merging work does not put it in front of anyone. This is the step that does: it moves this platform — or any project it hosts — onto a newer core, waits for that box to answer, and puts it back if it does not.",
6864
6864
  "file": "modules/hall-ui/public/deploy.html",
6865
6865
  "line": 39,
6866
6866
  "origin": "html-text",
@@ -6988,7 +6988,7 @@
6988
6988
  "surface": "builders-hall",
6989
6989
  "text": "Move to {…}",
6990
6990
  "file": "modules/hall-ui/public/deploy.js",
6991
- "line": 103,
6991
+ "line": 122,
6992
6992
  "origin": "js-markup",
6993
6993
  "confidence": "certain"
6994
6994
  },
@@ -6997,7 +6997,7 @@
6997
6997
  "surface": "builders-hall",
6998
6998
  "text": "Move to {…}",
6999
6999
  "file": "modules/hall-ui/public/deploy.js",
7000
- "line": 161,
7000
+ "line": 217,
7001
7001
  "origin": "js-text-assign",
7002
7002
  "confidence": "certain"
7003
7003
  },
@@ -7316,6 +7316,15 @@
7316
7316
  "origin": "js-markup",
7317
7317
  "confidence": "certain"
7318
7318
  },
7319
+ {
7320
+ "id": "2947c1ddea9a",
7321
+ "surface": "builders-hall",
7322
+ "text": "No running box to deploy.",
7323
+ "file": "modules/hall-ui/public/deploy.js",
7324
+ "line": 195,
7325
+ "origin": "js-markup",
7326
+ "confidence": "certain"
7327
+ },
7319
7328
  {
7320
7329
  "id": "9fb9f766450d",
7321
7330
  "surface": "builders-hall",
@@ -7591,7 +7600,7 @@
7591
7600
  "surface": "builders-hall",
7592
7601
  "text": "Nothing newer is published that this box can take. There is nothing to deploy.",
7593
7602
  "file": "modules/hall-ui/public/deploy.js",
7594
- "line": 126,
7603
+ "line": 140,
7595
7604
  "origin": "js-markup",
7596
7605
  "confidence": "certain"
7597
7606
  },
@@ -9247,7 +9256,7 @@
9247
9256
  "surface": "builders-hall",
9248
9257
  "text": "See what would change",
9249
9258
  "file": "modules/hall-ui/public/deploy.js",
9250
- "line": 91,
9259
+ "line": 108,
9251
9260
  "origin": "js-markup",
9252
9261
  "confidence": "certain"
9253
9262
  },
@@ -9315,11 +9324,11 @@
9315
9324
  "confidence": "certain"
9316
9325
  },
9317
9326
  {
9318
- "id": "fcd68c6a21fd",
9327
+ "id": "a39b0f9d86e5",
9319
9328
  "surface": "builders-hall",
9320
- "text": "Serving now",
9329
+ "text": "Serving {…} — read from the box itself, {…}.",
9321
9330
  "file": "modules/hall-ui/public/deploy.js",
9322
- "line": 115,
9331
+ "line": 142,
9323
9332
  "origin": "js-markup",
9324
9333
  "confidence": "certain"
9325
9334
  },
@@ -10944,11 +10953,20 @@
10944
10953
  "confidence": "certain"
10945
10954
  },
10946
10955
  {
10947
- "id": "77e3b8f5318b",
10956
+ "id": "f93fcb24c6ba",
10948
10957
  "surface": "builders-hall",
10949
- "text": "This restarts the platform onto core",
10958
+ "text": "This platform",
10950
10959
  "file": "modules/hall-ui/public/deploy.js",
10951
- "line": 103,
10960
+ "line": 187,
10961
+ "origin": "js-markup",
10962
+ "confidence": "certain"
10963
+ },
10964
+ {
10965
+ "id": "6e9dfe1fb903",
10966
+ "surface": "builders-hall",
10967
+ "text": "This restarts {…} onto core",
10968
+ "file": "modules/hall-ui/public/deploy.js",
10969
+ "line": 122,
10952
10970
  "origin": "js-markup",
10953
10971
  "confidence": "certain"
10954
10972
  },
@@ -12802,7 +12820,7 @@
12802
12820
  "surface": "builders-hall",
12803
12821
  "text": "is published and this box is not on it yet.",
12804
12822
  "file": "modules/hall-ui/public/deploy.js",
12805
- "line": 91,
12823
+ "line": 108,
12806
12824
  "origin": "js-markup",
12807
12825
  "confidence": "certain"
12808
12826
  },
@@ -12955,7 +12973,7 @@
12955
12973
  "surface": "builders-hall",
12956
12974
  "text": "no reading",
12957
12975
  "file": "modules/hall-ui/public/deploy.js",
12958
- "line": 74,
12976
+ "line": 91,
12959
12977
  "origin": "js-markup",
12960
12978
  "confidence": "certain"
12961
12979
  },
@@ -13225,7 +13243,7 @@
13225
13243
  "surface": "builders-hall",
13226
13244
  "text": "reads the control plane's own observation · ADR 0293 · every move is recorded with who asked",
13227
13245
  "file": "modules/hall-ui/public/deploy.js",
13228
- "line": 132,
13246
+ "line": 199,
13229
13247
  "origin": "js-markup",
13230
13248
  "confidence": "certain"
13231
13249
  },
@@ -14138,15 +14156,6 @@
14138
14156
  "origin": "js-markup",
14139
14157
  "confidence": "certain"
14140
14158
  },
14141
- {
14142
- "id": "d9b4ae2db0f4",
14143
- "surface": "builders-hall",
14144
- "text": "{…} — read from the box itself, {…}.",
14145
- "file": "modules/hall-ui/public/deploy.js",
14146
- "line": 115,
14147
- "origin": "js-markup",
14148
- "confidence": "certain"
14149
- },
14150
14159
  {
14151
14160
  "id": "085f93f3f775",
14152
14161
  "surface": "builders-hall",
@@ -14156,15 +14165,6 @@
14156
14165
  "origin": "js-markup",
14157
14166
  "confidence": "certain"
14158
14167
  },
14159
- {
14160
- "id": "b814cb35ce2f",
14161
- "surface": "builders-hall",
14162
- "text": "{…} — the box this platform runs on",
14163
- "file": "modules/hall-ui/public/deploy.js",
14164
- "line": 168,
14165
- "origin": "js-text-assign",
14166
- "confidence": "certain"
14167
- },
14168
14168
  {
14169
14169
  "id": "f75ee96a7511",
14170
14170
  "surface": "builders-hall",
@@ -2218,5 +2218,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2218
2218
  landed since 1.19.870 with no explicit bump. run 35393164740. (task 1002620)
2219
2219
  1.19.872 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2220
2220
  landed since 1.19.871 with no explicit bump. run 35419318693. (task 1002620)
2221
+ 1.19.873 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2222
+ landed since 1.19.872 with no explicit bump. run 35419621747. (task 1002620)
2223
+ 1.19.874 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2224
+ landed since 1.19.873 with no explicit bump. run 35419921833. (task 1002620)
2221
2225
  ---------------------------------------------------------------------------
2222
2226
  ```
@@ -23,10 +23,11 @@ The generated typed client (`node scripts/gds/gen-api-client.js` → `clients/bo
23
23
  These involve payment and permanent, account-level actions. They are **not** done inside a build session.
24
24
 
25
25
  1. **Put the `bongos` npm org on a paid plan.** Private scoped publishing requires it. On npmjs.com → your org → Billing → upgrade to a plan that allows private packages (npm Teams, ~$7/member/month). Add as members only the people who need to publish or install during pre-launch — each is a billed seat.
26
- 2. **Create an access token.** npm retired classic (Automation/Publish) tokens — use a **granular access token**: npmjs.com → Access Tokens → *Generate New Token* → **Granular Access Token**, scoped to the `@bongos` packages with **Read and write** permission, and — for a token that publishes — enable **"Bypass two-factor authentication"** (npm otherwise requires interactive 2FA at publish). A **Read-only** granular token is enough for machines that only install. Granular tokens **expire** (pick the longest expiry the policy allows and calendar the rotation — a silently expired token is a 404 on the private package, the same symptom as a missing one). Store it as an environment variable, never in a committed file:
27
- ```sh
28
- export NPM_TOKEN=<redacted>
29
- ```
26
+ 2. **Register a trusted publisher — do NOT create a token.** The automatic lane publishes with **OIDC trusted publishing** ([ADR 0180](../adr/0180-dependency-intake-and-drift-policy.md) decision 4, [task 1003202](https://cloudbongos.com/builders#/task/1003202)): npmjs.com → the `@bongos/core` package → Settings → Trusted publisher → GitHub Actions, naming this repo and the workflow filename `publish.yml`. The runner mints a short-lived identity token per run; nothing long-lived exists to steal or rotate. **If the workflow is ever renamed, update the trusted-publisher entry in the same change** or every publish starts failing 403.
27
+
28
+ A **manual** `package-core.js --publish --live` still needs a human `npm login` (+ 2FA) on that machine — an interactive session, not a stored credential.
29
+
30
+ > **No install token, and none can be issued to any purpose.** `@bongos/core` is public ([ADR 0283](../adr/0283-the-core-publishes-publicly-because-access-was-never-the-thing-protecting-it.md)), so nothing needs a credential to *read* it, and [`fcea51ca`](https://github.com/example-owner/cloud-bongos/commit/fcea51ca) deleted the auth plumbing that used to consume one. A read-only `NPM_TOKEN` in an environment here buys nothing and is one more thing to leak — which is exactly how it leaked: the value sat in the droplet's journal in cleartext for a week (blocker 1000118).
30
31
 
31
32
  ## Publish (the real, permanent step — owner)
32
33
 
@@ -54,7 +55,7 @@ npm publish ./clients/bongos-client # real (publishConfig makes it
54
55
 
55
56
  ## Cutting a core release (the policy — [ADR 0161](../adr/0161-publish-on-merge.md), superseding ADR 0137)
56
57
 
57
- **Publishing is automatic on merge** as of 2026-08-08: the `publish` workflow rides behind the required `unit` check, auto-bumps a patch when nobody bumped, builds through the fail-closed no-leak gate, publishes, and tags — see the workflow header (`.github/workflows/publish.yml`) and ADR 0161. It ships inert until the owner arms it (repo variable `PUBLISH_ON_MERGE=1` + secrets `NPM_TOKEN`, `RELEASE_PUSH_TOKEN`). A **minor/major bump stays a human in-task edit**. The update subscription ([ADR 0136](../adr/0136-update-channel-subscription-policy.md)) consumes whatever is published, on its next sweep.
58
+ **Publishing is automatic on merge** as of 2026-08-08: the `publish` workflow rides behind the required `unit` check, auto-bumps a patch when nobody bumped, builds through the fail-closed no-leak gate, publishes, and tags — see the workflow header (`.github/workflows/publish.yml`) and ADR 0161. It ships inert until the owner arms it: repo variable `PUBLISH_ON_MERGE=1`, a **trusted publisher** registered on npmjs.com for `@bongos/core` naming this repo + `publish.yml`, and the secret `RELEASE_PUSH_TOKEN` (contents:write, for the bump commit). There is deliberately **no** `NPM_TOKEN` — the lane authenticates by OIDC, and the workflow sets neither `NPM_TOKEN` nor `NODE_AUTH_TOKEN` because either one would send npm down the legacy token path and skip the OIDC exchange entirely. A **minor/major bump stays a human in-task edit**. The update subscription ([ADR 0136](../adr/0136-update-channel-subscription-policy.md)) consumes whatever is published, on its next sweep.
58
59
 
59
60
  The steps below remain as the **manual fallback** (lane disarmed, npm outage, or a deliberate out-of-band release):
60
61
 
@@ -71,35 +72,29 @@ The steps below remain as the **manual fallback** (lane disarmed, npm outage, or
71
72
 
72
73
  > **Automating this** (a tag-triggered CI publish) is deferred to the managed-fleet epic ([#1948](https://example.com/builders#/task/1948)); revisit when the manual step becomes a real bottleneck. See [ADR 0137](../adr/0137-upstream-publish-policy-manual-owner-gated.md) "When to revisit".
73
74
 
74
- ## Install a private package (any consumer machine or instance)
75
-
76
- Add a one-line `.npmrc` (repo-root or `~/.npmrc`) that reads the token from the environment:
75
+ ## Install the core (any consumer machine or instance)
77
76
 
78
- ```
79
- //registry.npmjs.org/:_authToken=<redacted>
77
+ ```sh
78
+ npm install -g @bongos/core # global → the `bongos` command, then `bongos init`
80
79
  ```
81
80
 
82
- Then, with `NPM_TOKEN` set in the environment:
81
+ That is the whole step. No `.npmrc`, no token, no org membership — `@bongos/core` is public ([ADR 0283](../adr/0283-the-core-publishes-publicly-because-access-was-never-the-thing-protecting-it.md)). Verify from a clean environment rather than trusting a logged-in shell, which would succeed either way:
83
82
 
84
83
  ```sh
85
- npm install -g @bongos/core # global → the `bongos` command, then `bongos init`
84
+ npm pack @bongos/core --userconfig /dev/null # no credential is read at all
86
85
  ```
87
86
 
88
87
  (A project that calls the API vendors the generated client instead — it is not installable.)
89
88
 
90
- Without a valid token + org membership the install 404s — that is the private gate working.
91
-
92
- ## Provisioning: registry pull vs vendored tarball (task 2090)
89
+ > **This section used to say the opposite**, and the difference was invisible: a tokenless read of a *restricted* package returns a flat `E404`, indistinguishable from "no such package". That one ambiguity is what killed the nightly auto-upgrade sweep silently for three weeks, reporting `failed=0` the whole time — the sweep's unit passed no `NPM_TOKEN`, the env-fed `.npmrc` expanded to empty, and a skip is not a failure in that script's accounting. Do not reintroduce an install credential here.
93
90
 
94
- Provisioning defaults to installing the core from a **vendored tarball** committed into each instance repo (task 2053) — offline, needs no token, and stays the default/fallback until the registry path is proven on a real greenfield standup. The registry pull is **opt-in and fail-soft**:
91
+ ## Provisioning: how a new instance gets its core
95
92
 
96
- - **Turn it on** by setting `PROVISION_CORE_FROM_REGISTRY=1` in the control-plane runner's environment (e.g. `/etc/cloudbongos/box.env`) **and** providing a read-only `NPM_TOKEN` (a **granular access token** — Read-only on the `@bongos` packages — from the paid `bongos` org; mind its expiry). With both present, the standalone scaffold pins `@bongos/core@<version>` from the registry and writes an env-fed `.npmrc` (`_authToken=<redacted> instead of vendoring a tarball; `npm ci` then pulls the private package (the token is inherited from the runner env — never committed).
97
- - **Fail-soft**: if the flag is set but `NPM_TOKEN` is absent, provisioning logs a notice and **falls back to the vendored tarball**, so a tokenless standup still works.
98
- - **Directly**: `bongos init --registry-core` scaffolds the registry shape (caret pin + `.npmrc`; generates the lockfile when `NPM_TOKEN` is set). `bongos upgrade --to <v> --registry` bumps a consumer to a registry version. Both are mutually exclusive with the vendored `--vendor-core` / `--from` inputs.
93
+ Provisioning vendors a **tarball** into each new instance repo, and the instance pins it as `file:vendor/bongos-core-<v>.tgz`. `resolveVendorTarball` in [`provision-repo.js`](../../scripts/gds/provision-repo.js) walks a ladder for the freshest correct one: reuse the control plane's own `file:`-pinned tarball → `npm pack` the control plane's registry-installed core ([ADR 0168](../adr/0168-scaffold-packs-installed-core.md)) → rebuild from source. A child is therefore born on the same core its parent is actually running.
99
94
 
100
- A tokenless registry `npm ci` 404s cleanly (`E404 … @bongos/core … could not be found or you do not have permission to access it`) — the expected pre-launch outcome.
95
+ **There is no token in any of this, and no opt-in fork either.** An earlier design gated a registry pull behind `PROVISION_CORE_FROM_REGISTRY=1` plus a read-only `NPM_TOKEN`, fail-soft to vendoring when the credential was absent. [`fcea51ca`](https://github.com/example-owner/cloud-bongos/commit/fcea51ca) deleted that whole path along with `scripts/gds/npmrc.js`: two install modes diverging on an environment variable is a defect shape, and the credential they diverged on stopped being necessary the moment the package went public. `bongos init --registry-core` and the env-fed `.npmrc` it used to write are gone with it.
101
96
 
102
- > **Proven end-to-end (2026-07-08, task 2118 / blocker 71).** A real `bongos init --registry-core` scaffold, with a valid read-only token in `NPM_TOKEN`, resolved `@bongos/core@1.17.0` straight from `registry.npmjs.org` and generated a reproducible `package-lock.json`. Both legs are confirmed: the registry pull works with a token and fails closed (404) without one. Provisioning still defaults to the vendored tarball until an owner explicitly sets `PROVISION_CORE_FROM_REGISTRY=1` on a live runner.
97
+ > **Vendoring is not a stale-by-design choice.** It is what keeps twenty co-tenant instances pinned to a local path with no registry channel to follow — going public was necessary to fix that, and is not sufficient ([ADR 0283](../adr/0283-the-core-publishes-publicly-because-access-was-never-the-thing-protecting-it.md)). Moving a co-tenant forward is still the `package-core` → scp → `bongos upgrade` path.
103
98
 
104
99
  ## Going public — DONE 2026-09-13
105
100
 
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Deploy — {{worldName}}</title>
7
- <meta name="description" content="Move this instance onto a newer core and read back the version the box actually serves — the deploy gate, without a shell or an ssh key.">
7
+ <meta name="description" content="Move this platform or any project it hosts onto a newer core, and read back the version each box actually serves — the deploy gate, without a shell or an ssh key.">
8
8
  <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -38,8 +38,8 @@
38
38
  mean visible (ADR 0137's three gates); this page is the third one. -->
39
39
  <p class="scroll__lede">
40
40
  Merging work does not put it in front of anyone. This is the step that does:
41
- it moves this instance onto a newer core, waits for the box to answer, and
42
- puts itself back if it does not.
41
+ it moves this platform — or any project it hosts — onto a newer core, waits
42
+ for that box to answer, and puts it back if it does not.
43
43
  </p>
44
44
  <!-- One mount, written by deploy.js. Nothing here is authored as markup with
45
45
  a version in it: every number on this page is a reading the control plane
@@ -59,6 +59,6 @@
59
59
  <script src="/builders/palette.js" defer></script>
60
60
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
61
61
  <script src="/builders/hall-kit.js"></script>
62
- <script src="/builders/deploy.js?v=2026-09-16-deploy1"></script>
62
+ <script src="/builders/deploy.js?v=2026-09-18-deploy2"></script>
63
63
  </body>
64
64
  </html>
@@ -1,25 +1,43 @@
1
- // deploy.js — the owner's deploy door (task 1003159, ADR 0293).
1
+ // deploy.js — the owner's deploy door (task 1003159, ADR 0293; widened to the
2
+ // hosted projects by task 1004046).
2
3
  //
3
4
  // WHAT THIS PAGE IS FOR. The owner, twice in a month: "I don't think I can make anything
4
- // live", then "this is above my paygrade." Moving this instance's core pin is gate 3 of
5
- // the release pipeline and the ONLY act that makes shipped work visible to users — and it
6
- // was reachable from exactly one place, a shell on the control-plane box holding an ssh
7
- // key. An agent could not close the loop either: the permission classifier blocks a
8
- // mutating `sudo -u lars` over ssh by design. So the one gate that is the owner's to hold
9
- // was the one gate with no instrument. This page is the instrument.
5
+ // live", then "this is above my paygrade." Moving a core pin is gate 3 of the release
6
+ // pipeline and the ONLY act that makes shipped work visible to users — and it was
7
+ // reachable from exactly one place, a shell on the control-plane box holding an ssh key.
8
+ // An agent could not close the loop either: the permission classifier blocks a mutating
9
+ // `sudo -u lars` over ssh by design. So the one gate that is the owner's to hold was the
10
+ // one gate with no instrument. This page is the instrument.
11
+ //
12
+ // WHY THE PROJECTS ARE HERE AND NOT IN THEIR OWN HALLS. The obvious shape — each owner
13
+ // pressing Upgrade in their own project's hall — is not buildable: the upgrade route and
14
+ // the intent queue belong to the PROVISIONING module, and a hosted project does not run
15
+ // it. Probed 2026-09-18: /api/bongos/provisioning/health answers 200 here and 404 on
16
+ // every tenant. The control plane is the only place that can act on them, so this is
17
+ // where the button lives. Their own halls get the banner instead (task 1004045), which
18
+ // each instance derives for itself from the registry and needs nothing from us.
19
+ //
20
+ // Nothing about the ENGINE had to change for this. UPGRADE_SHAPES already admitted
21
+ // 'co-tenant' and 'standalone' alongside 'control-plane', coreUpgradeInstance already
22
+ // resolved each shape's own checkout and built that box's own health and version URLs,
23
+ // and the preview route already took any instance id. Only this page was single-row.
10
24
  //
11
25
  // THE RULE THAT SHAPES EVERY LINE HERE: this page KNOWS NOTHING. Every version on it is a
12
26
  // reading the control-plane runner took on the box, carried through the server with the
13
27
  // time it was taken. Nothing is computed here, no version is compared here, and "up to
14
28
  // date" is never inferred from an absence — the server says `behind` or it does not, and
15
29
  // a missing reading renders as a missing reading. A deploy control that guesses is worse
16
- // than none, because it is trusted.
30
+ // than none, because it is trusted. That rule is now load-bearing for boxes the owner has
31
+ // never logged into, which is the harder case, not the easier one.
17
32
  //
18
33
  // TWO STEPS, NEVER ONE (ADR 0293 D4). The owner asked for the confirmation. Auto-rollback
19
34
  // already guards the bad release; this guards the wrong button. Step one shows what would
20
35
  // move; step two names the version it saw and sends that. The confirm button is built
21
36
  // FROM the previewed version, so a release that lands between the two steps cannot
22
- // silently change what the press does.
37
+ // silently change what the press does. With several boxes on one page the step is held
38
+ // PER BOX, and each button's handler CLOSES OVER the preview object it was built from —
39
+ // so the version sent is the one that box displayed, and a repaint cannot swap it. A
40
+ // page-wide "currently previewing" would be a wrong-box press waiting to happen.
23
41
  (() => {
24
42
  'use strict';
25
43
 
@@ -29,10 +47,9 @@
29
47
  const toast = (msg) => window.OTB.toast(msg);
30
48
  const $ = (id) => document.getElementById(id);
31
49
 
32
- // The preview the server last handed us. The confirm step reads its version from HERE
33
- // rather than from the DOM, so what is sent is what was previewed even if a poll
34
- // repaints between the two clicks.
35
- let previewed = null;
50
+ // instance_id -> 'preview' | 'confirm'. Held per box so opening the confirm on one does
51
+ // not arm it on another.
52
+ const steps = new Map();
36
53
 
37
54
  async function fetchJson(path) {
38
55
  const res = await api.request('GET', `${API}${path}`);
@@ -74,7 +91,7 @@
74
91
  return v ? `<span class="ov-mono">${escapeHtml(String(v))}</span>` : '<span class="ps-pill ps-pill--absent">no reading</span>';
75
92
  }
76
93
 
77
- // ---- the three states this page can be in --------------------------------
94
+ // ---- the states ONE box can be in ----------------------------------------
78
95
 
79
96
  // Something is already running. The press is not offered at all — offering a button
80
97
  // that the server will refuse with 'intent_conflict' is a worse experience than saying
@@ -92,69 +109,108 @@
92
109
  <p class="ov-row__sub">Core <strong>${escapeHtml(String(p.recommended_version))}</strong> is published and
93
110
  this box is not on it yet.</p>
94
111
  <div class="ps-controls">
95
- <button type="button" class="btn" id="dp-preview">See what would change</button>
112
+ <button type="button" class="btn" id="dp-preview-${escapeHtml(String(p.instance_id))}">See what would change</button>
96
113
  </div>`;
97
114
  }
98
115
 
99
- // Step two. The version is written into the button's own label and into `previewed`,
100
- // so the owner confirms the thing they were shown rather than "the newest", which is a
101
- // different act by the time it is resolved.
102
- function stepTwoHtml(p) {
116
+ // Step two. The version is written into the button's own label, and the handler wired
117
+ // to that button closes over this same preview object — so the owner confirms the thing
118
+ // they were shown rather than "the newest", which is a different act by the time it is
119
+ // resolved.
120
+ function stepTwoHtml(p, isSelf) {
121
+ const id = escapeHtml(String(p.instance_id));
103
122
  return `
104
- <div class="ps-controls" id="dp-confirm-wrap">
105
- <p class="ov-row__sub">This restarts the platform onto core
123
+ <div class="ps-controls">
124
+ <p class="ov-row__sub">This restarts ${isSelf ? 'the platform' : escapeHtml(String(p.slug))} onto core
106
125
  <strong>${escapeHtml(String(p.recommended_version))}</strong>. It waits for the box to answer and
107
126
  puts itself back on ${escapeHtml(String(p.served_version))} if it does not.</p>
108
- <button type="button" class="btn btn--primary" id="dp-confirm">Move to ${escapeHtml(String(p.recommended_version))}</button>
109
- <button type="button" class="btn" id="dp-cancel">Cancel</button>
127
+ <button type="button" class="btn btn--primary" id="dp-confirm-${id}">Move to ${escapeHtml(String(p.recommended_version))}</button>
128
+ <button type="button" class="btn" id="dp-cancel-${id}">Cancel</button>
110
129
  </div>`;
111
130
  }
112
131
 
113
- function panelHtml(p, step) {
114
- const rows = [
115
- `<li class="ov-row"><div class="ov-row__main">
116
- <span class="ov-row__title">Serving now</span>
117
- <p class="ov-row__sub">${versionHtml(p.served_version)} — read from the box itself, ${escapeHtml(ago(p.observed_at))}.</p>
118
- ${p.reading === 'no_answer'
119
- ? `<p class="ps-invalid">The box did not answer when the control plane last looked, so this version is unknown rather than unchanged. A move is not offered while that is true.</p>`
120
- : ''}
121
- </div></li>`,
122
- ];
123
- let action = '';
132
+ // One box's card. `isSelf` only changes the words, never what is offered: the runner
133
+ // treats the platform's own row and a tenant's identically, and a page that implied
134
+ // otherwise would be describing a rule that does not exist.
135
+ function boxHtml(p, isSelf) {
136
+ const step = steps.get(p.instance_id) || 'preview';
137
+ let action;
124
138
  if (p.open_intent) action = busyHtml(p);
125
139
  else if (p.reading === 'no_answer') action = '';
126
140
  else if (!p.behind) action = `<p class="ov-row__sub">Nothing newer is published that this box can take. There is nothing to deploy.</p>`;
127
- else action = step === 'confirm' ? stepTwoHtml(p) : stepOneHtml(p);
128
- rows.push(`<li class="ov-row"><div class="ov-row__main">
129
- <span class="ov-row__title">Make it live</span>
141
+ else action = step === 'confirm' ? stepTwoHtml(p, isSelf) : stepOneHtml(p);
142
+ return `
143
+ <li class="ov-row" id="dp-box-${escapeHtml(String(p.instance_id))}"><div class="ov-row__main">
144
+ <span class="ov-row__title">${escapeHtml(String(p.slug))}${isSelf ? ' — the box this platform runs on' : ''}</span>
145
+ <p class="ov-row__sub">Serving ${versionHtml(p.served_version)} — read from the box itself, ${escapeHtml(ago(p.observed_at))}.</p>
146
+ ${p.reading === 'no_answer'
147
+ ? `<p class="ps-invalid">The box did not answer when the control plane last looked, so this version is unknown rather than unchanged. A move is not offered while that is true.</p>`
148
+ : ''}
130
149
  ${action}
131
- </div></li>`);
132
- return `<ul class="ov-lines">${rows.join('')}</ul>
133
- <p class="ps-prov">reads the control plane's own observation · ADR 0293 · every move is recorded with who asked</p>`;
150
+ </div></li>`;
151
+ }
152
+
153
+ // A project whose preview could not be read at all. It is LISTED rather than dropped:
154
+ // a box silently missing from a deploy page reads as a box that does not need
155
+ // deploying, which is the one wrong impression this page must never give.
156
+ function unreadableHtml(inst, message) {
157
+ return `
158
+ <li class="ov-row"><div class="ov-row__main">
159
+ <span class="ov-row__title">${escapeHtml(String(inst.slug))}</span>
160
+ <p class="ps-invalid">Could not read this project's deploy state: ${escapeHtml(message)}</p>
161
+ </div></li>`;
134
162
  }
135
163
 
136
164
  // ---- wiring --------------------------------------------------------------
137
165
 
138
- function render(p, step) {
139
- previewed = p;
140
- $('dp-panel').innerHTML = panelHtml(p, step);
141
- const preview = $('dp-preview');
142
- if (preview) preview.addEventListener('click', () => render(p, 'confirm'));
143
- const cancel = $('dp-cancel');
144
- if (cancel) cancel.addEventListener('click', () => render(p, 'preview'));
145
- const confirm = $('dp-confirm');
166
+ function wire(p) {
167
+ const id = String(p.instance_id);
168
+ const preview = $(`dp-preview-${id}`);
169
+ if (preview) preview.addEventListener('click', () => { steps.set(p.instance_id, 'confirm'); repaint(); });
170
+ const cancel = $(`dp-cancel-${id}`);
171
+ if (cancel) cancel.addEventListener('click', () => { steps.set(p.instance_id, 'preview'); repaint(); });
172
+ const confirm = $(`dp-confirm-${id}`);
146
173
  if (confirm) confirm.addEventListener('click', () => move(p));
147
174
  }
148
175
 
176
+ // What the page is currently showing: the self row (or null), the readable project
177
+ // previews, and the ones that would not read. Held so a repaint after a click does not
178
+ // have to re-fetch everything.
179
+ let view = { self: null, boxes: [], unreadable: [], notEnrolled: null };
180
+
181
+ function repaint() {
182
+ const items = [];
183
+ if (view.self) items.push(boxHtml(view.self, true));
184
+ else if (view.notEnrolled) {
185
+ // The server's own sentence names the one command that fixes it, which is why it
186
+ // is shown rather than a generic failure line.
187
+ items.push(`<li class="ov-row"><div class="ov-row__main">
188
+ <span class="ov-row__title">This platform</span>
189
+ <p class="ps-invalid">${escapeHtml(view.notEnrolled)}</p>
190
+ </div></li>`);
191
+ }
192
+ for (const p of view.boxes) items.push(boxHtml(p, false));
193
+ for (const u of view.unreadable) items.push(unreadableHtml(u.inst, u.message));
194
+ if (!items.length) {
195
+ items.push(`<li class="ov-row"><div class="ov-row__main">
196
+ <p class="ov-row__sub">No running box to deploy.</p>
197
+ </div></li>`);
198
+ }
199
+ $('dp-panel').innerHTML = `<ul class="ov-lines">${items.join('')}</ul>
200
+ <p class="ps-prov">reads the control plane's own observation · ADR 0293 · every move is recorded with who asked</p>`;
201
+ if (view.self) wire(view.self);
202
+ for (const p of view.boxes) wire(p);
203
+ }
204
+
149
205
  async function move(p) {
150
- const btn = $('dp-confirm');
206
+ const btn = $(`dp-confirm-${p.instance_id}`);
151
207
  if (btn) { btn.disabled = true; btn.textContent = 'Queueing…'; }
152
208
  try {
153
- // `p.recommended_version` — the version this owner was SHOWN, not whatever is
154
- // newest at the moment of the click.
209
+ // `p.recommended_version` — the version this owner was SHOWN for THIS box, not
210
+ // whatever is newest at the moment of the click.
155
211
  const r = await sendJson('POST', `/provisioning/instances/${p.instance_id}/core-upgrade`, { to: p.recommended_version });
156
212
  toast(r.message || 'Queued.');
157
- await load();
213
+ await refreshOne(p);
158
214
  } catch (e) {
159
215
  if (e && e.kind === '401') return;
160
216
  toast(e.message || 'Could not queue the move.');
@@ -162,12 +218,90 @@
162
218
  }
163
219
  }
164
220
 
221
+ // Re-read ONLY the box that just changed. The full load() fans out one probe per
222
+ // hosted project, and firing all of them to refresh one row is cache invalidation
223
+ // far wider than the change — it also re-probes boxes the owner never touched. A
224
+ // failure here falls back to the full read, because a stale row on a deploy page is
225
+ // the one thing worse than an extra request.
226
+ async function refreshOne(p) {
227
+ const id = String(p.instance_id);
228
+ const isSelf = view.self && String(view.self.instance_id) === id;
229
+ try {
230
+ const d = await fetchJson(isSelf ? '/provisioning/control-plane' : `/provisioning/instances/${id}/core-upgrade`);
231
+ if (isSelf) view.self = d.preview;
232
+ else view.boxes = view.boxes.map((b) => (String(b.instance_id) === id ? d.preview : b));
233
+ steps.set(p.instance_id, 'preview');
234
+ repaint();
235
+ } catch (e) {
236
+ if (e && e.kind === '401') return;
237
+ await load();
238
+ }
239
+ }
240
+
241
+ // The projects this control plane hosts. `/provisioning/fleet` is the whole roster and
242
+ // needs its own atom; a holder of core.pin.move who lacks it still gets their OWN
243
+ // projects rather than an error, because the pin move itself is gated server-side per
244
+ // instance either way — this choice only decides what the page can LIST.
245
+ async function hostedProjects() {
246
+ try {
247
+ const d = await fetchJson('/provisioning/fleet');
248
+ return d.instances || [];
249
+ } catch (e) {
250
+ if (e && e.kind === '403') {
251
+ const d = await fetchJson('/provisioning/instances');
252
+ return d.instances || [];
253
+ }
254
+ throw e;
255
+ }
256
+ }
257
+
258
+ // Only a RUNNING box can have its pin moved, and the runner refuses every other shape.
259
+ // Filtering here keeps torn-down rows — there are far more of them than live ones — off
260
+ // a page whose whole job is naming what is live.
261
+ const UPGRADEABLE = new Set(['standalone', 'co-tenant']);
262
+ const isLive = (i) => i && i.status === 'active' && UPGRADEABLE.has(i.hosting_shape);
263
+
165
264
  async function load() {
166
265
  try {
167
- const d = await fetchJson('/provisioning/control-plane');
168
- $('dp-sub').textContent = `${d.preview.slug} — the box this platform runs on`;
266
+ let self = null;
267
+ let notEnrolled = null;
268
+ try {
269
+ const d = await fetchJson('/provisioning/control-plane');
270
+ self = d.preview;
271
+ } catch (e) {
272
+ if (e && e.kind === '401') return;
273
+ if (e && e.kind === '403') throw e;
274
+ notEnrolled = e.message || 'This platform has no deploy door yet.';
275
+ }
276
+
277
+ const live = (await hostedProjects()).filter(isLive);
278
+ // One preview per project, in parallel and INDEPENDENTLY: one project that cannot
279
+ // be read must not blank the page for the others. The request count scales with the
280
+ // fleet, which is deliberate and bounded — each is a cached server-side reading, not
281
+ // a live probe of the box, and this page exists precisely to show every box's state
282
+ // at once. It is re-read on OPEN only; a move refreshes just its own row.
283
+ const settled = await Promise.all(live.map(async (inst) => {
284
+ try {
285
+ const d = await fetchJson(`/provisioning/instances/${inst.id}/core-upgrade`);
286
+ return { ok: true, preview: d.preview };
287
+ } catch (e) {
288
+ if (e && e.kind === '401') return { ok: false, inst, message: 'not signed in' };
289
+ return { ok: false, inst, message: (e && e.message) || 'unknown error' };
290
+ }
291
+ }));
292
+
293
+ view = {
294
+ self,
295
+ notEnrolled,
296
+ boxes: settled.filter((s) => s.ok).map((s) => s.preview),
297
+ unreadable: settled.filter((s) => !s.ok).map((s) => ({ inst: s.inst, message: s.message })),
298
+ };
299
+ const n = view.boxes.length + view.unreadable.length;
300
+ $('dp-sub').textContent = n
301
+ ? `This platform and ${n} hosted project${n === 1 ? '' : 's'}`
302
+ : 'This platform';
169
303
  $('dp-body').hidden = false;
170
- render(d.preview, 'preview');
304
+ repaint();
171
305
  } catch (e) {
172
306
  if (e && e.kind === '401') return;
173
307
  if (e && e.kind === '403') {
@@ -175,9 +309,6 @@
175
309
  $('dp-sub').textContent = 'Restricted';
176
310
  return;
177
311
  }
178
- // Including 'control_plane_not_enrolled', whose message names the one command
179
- // that fixes it — which is why the server's sentence is shown rather than a
180
- // generic failure line.
181
312
  $('dp-body').hidden = false;
182
313
  $('dp-sub').textContent = 'Unavailable';
183
314
  $('dp-panel').innerHTML = `<p class="ps-invalid">${escapeHtml(e.message || 'Could not read the deploy state.')}</p>`;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.872",
3
+ "version": "1.19.874",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.872",
9
+ "version": "1.19.874",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.872",
3
+ "version": "1.19.874",
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",
@@ -374,8 +374,10 @@ function installedCoreTarball({ instanceRoot, fsImpl = fs } = {}) {
374
374
  // abort of task 1002275, resurrected by the cutover). The installed package itself is
375
375
  // still the exact core this hub runs: return its node_modules dir + version + the
376
376
  // `npm pack` tarball name so the scaffold can pack it into a vendorable .tgz. The child
377
- // stays file:-pinned — its owner needs no npm token (the registry package is PRIVATE
378
- // until the ADR 0134 go-public step). Returns { dir, version, tarballName } or null
377
+ // stays file:-pinned — its owner needs no npm token. (That was originally because the
378
+ // registry package was PRIVATE; since ADR 0283 it is public and a token would buy
379
+ // nothing either way. The file: pin is kept for the reason in ADR 0168 — a child is born
380
+ // on the core its parent actually runs — not for access control.) Returns { dir, version, tarballName } or null
379
381
  // (a file: pin, an uninstalled dep, or a source checkout). fs reads only → dry-run safe.
380
382
  function installedCorePackDir({ instanceRoot, fsImpl = fs } = {}) {
381
383
  const root = instanceRoot || process.env.BONGOS_INSTANCE_ROOT || process.cwd();
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.872'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.874'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,193 @@
1
+ // tests/deploy_page_projects.mjs — the deploy page now drives every hosted project,
2
+ // not only the control plane's own row (task 1004046).
3
+ //
4
+ // These EXECUTE deploy.js in a DOM stub and read what it painted. The sibling suite
5
+ // (core_upgrade_door.mjs) asserts the page's invariants against its SOURCE, which is
6
+ // right for "never computes a version comparison" but cannot see the thing that broke
7
+ // when this page went multi-box: whether a click on one box arms that box and not
8
+ // another. That is behaviour, so it is tested as behaviour.
9
+ import assert from 'node:assert/strict';
10
+ import { test } from 'node:test';
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import vm from 'node:vm';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17
+ const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/deploy.js'), 'utf8');
18
+
19
+ const preview = (id, slug, over = {}) => ({
20
+ instance_id: id, slug, hosting_shape: 'standalone',
21
+ served_version: '1.19.800', recommended_version: '1.19.871',
22
+ behind: true, reading: 'ok', observed_at: new Date().toISOString(), open_intent: null,
23
+ ...over,
24
+ });
25
+
26
+ // The fleet as the control plane really reports it: mostly torn down, a couple live.
27
+ const FLEET = [
28
+ { id: '31', slug: 'mercury', status: 'active', hosting_shape: 'standalone' },
29
+ { id: '32', slug: 'hoodify-finance', status: 'active', hosting_shape: 'co-tenant' },
30
+ { id: '33', slug: 'walk-lantern', status: 'torn_down', hosting_shape: 'standalone' },
31
+ { id: '34', slug: 'testccc', status: 'torn_down', hosting_shape: 'standalone' },
32
+ { id: '35', slug: 'unreadable-one', status: 'active', hosting_shape: 'standalone' },
33
+ ];
34
+
35
+ function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus = 200, fleet = FLEET, fleetStatus = 200 } = {}) {
36
+ const els = new Map();
37
+ const posts = [];
38
+ const gets = [];
39
+ const makeEl = () => ({
40
+ _on: {}, _html: '', hidden: false, textContent: '', disabled: false,
41
+ addEventListener(ev, fn) { this._on[ev] = fn; },
42
+ set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; },
43
+ });
44
+ const documentObj = {
45
+ getElementById(id) { if (!els.has(id)) els.set(id, makeEl()); return els.get(id); },
46
+ addEventListener(ev, fn) { if (ev === 'DOMContentLoaded') this._ready = fn; },
47
+ };
48
+ const request = async (method, url, opts) => {
49
+ const p = url.replace('/api/bongos', '');
50
+ if (method === 'GET') gets.push(p);
51
+ if (method === 'POST') { posts.push({ path: p, body: opts && opts.body }); return { ok: true, status: 200, data: { message: 'Queued.' } }; }
52
+ if (p === '/provisioning/control-plane') {
53
+ if (controlPlaneStatus !== 200) {
54
+ return { ok: false, status: controlPlaneStatus, data: { error: { code: 'control_plane_not_enrolled', message: 'This platform has no deploy door yet. Run enroll-control-plane.js once.' } } };
55
+ }
56
+ return { ok: true, status: 200, data: { preview: controlPlane } };
57
+ }
58
+ if (p === '/provisioning/fleet') {
59
+ if (fleetStatus !== 200) return { ok: false, status: fleetStatus, data: { error: { code: 'forbidden' } } };
60
+ return { ok: true, status: 200, data: { instances: fleet } };
61
+ }
62
+ if (p === '/provisioning/instances') return { ok: true, status: 200, data: { instances: fleet } };
63
+ const m = /^\/provisioning\/instances\/(\w+)\/core-upgrade$/.exec(p);
64
+ if (m) {
65
+ const inst = fleet.find((f) => f.id === m[1]);
66
+ if (!inst || inst.slug === 'unreadable-one') {
67
+ return { ok: false, status: 500, data: { error: { code: 'core_upgrade_preview_failed', message: 'internal error' } } };
68
+ }
69
+ return { ok: true, status: 200, data: { preview: preview(inst.id, inst.slug, { hosting_shape: inst.hosting_shape }) } };
70
+ }
71
+ return { ok: false, status: 404, data: { error: { code: 'not_found' } } };
72
+ };
73
+ const windowObj = {
74
+ BongosClient: { createClient: () => ({ request }) },
75
+ OTB: { escapeHtml: (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])), toast() {} },
76
+ location: { assign() {} },
77
+ };
78
+ const sandbox = { window: windowObj, document: documentObj, console, setTimeout, clearTimeout, queueMicrotask, Date, Set, Map, Promise };
79
+ sandbox.globalThis = sandbox;
80
+ vm.createContext(sandbox);
81
+ vm.runInContext(SRC, sandbox, { filename: 'deploy.js' });
82
+ documentObj._ready();
83
+ const settle = async () => { for (let i = 0; i < 12; i++) await new Promise((r) => setTimeout(r, 0)); };
84
+ return { els, posts, gets, settle, panel: () => els.get('dp-panel').innerHTML, sub: () => els.get('dp-sub').textContent };
85
+ }
86
+
87
+ test('the platform AND every live hosted project get a row', async () => {
88
+ const b = boot();
89
+ await b.settle();
90
+ const html = b.panel();
91
+ assert.match(html, /cloudbongos/, 'the platform is still here');
92
+ assert.match(html, /mercury/, 'a standalone project');
93
+ assert.match(html, /hoodify-finance/, 'a co-tenant project');
94
+ assert.match(b.sub(), /3 hosted projects/);
95
+ });
96
+
97
+ test('torn-down projects never appear — the page is about what is live', async () => {
98
+ const b = boot();
99
+ await b.settle();
100
+ const html = b.panel();
101
+ assert.doesNotMatch(html, /walk-lantern/);
102
+ assert.doesNotMatch(html, /testccc/);
103
+ });
104
+
105
+ test('a project whose state cannot be read is LISTED, not silently dropped', async () => {
106
+ // A box missing from a deploy page reads as a box that needs no deploying.
107
+ const b = boot();
108
+ await b.settle();
109
+ assert.match(b.panel(), /unreadable-one/);
110
+ assert.match(b.panel(), /Could not read this project/);
111
+ });
112
+
113
+ test('opening the confirm on one project does not arm another', async () => {
114
+ const b = boot();
115
+ await b.settle();
116
+ b.els.get('dp-preview-31')._on.click();
117
+ const html = b.panel();
118
+ assert.match(html, /id="dp-confirm-31"/, 'mercury is armed');
119
+ assert.doesNotMatch(html, /id="dp-confirm-32"/, 'hoodify-finance is NOT');
120
+ assert.match(html, /id="dp-preview-32"/, 'and is still at step one');
121
+ });
122
+
123
+ test('confirming sends THAT project’s id and the version it was shown', async () => {
124
+ const b = boot();
125
+ await b.settle();
126
+ b.els.get('dp-preview-32')._on.click();
127
+ b.els.get('dp-confirm-32')._on.click();
128
+ await b.settle();
129
+ assert.equal(b.posts.length, 1);
130
+ assert.equal(b.posts[0].path, '/provisioning/instances/32/core-upgrade');
131
+ // Field-wise, not deepEqual: the body is built inside the vm realm, so its
132
+ // prototype is not this realm's Object and deepStrictEqual rejects it on identity
133
+ // alone — a harness artifact that says nothing about the payload.
134
+ assert.equal(b.posts[0].body.to, '1.19.871');
135
+ assert.deepEqual(Object.keys(b.posts[0].body), ['to'], 'and nothing else is sent');
136
+ });
137
+
138
+ test('a box with nothing newer offers no button', async () => {
139
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { behind: false, recommended_version: null }) });
140
+ await b.settle();
141
+ assert.match(b.panel(), /There is nothing to deploy/);
142
+ });
143
+
144
+ test('a box that did not answer is never rendered as up to date', async () => {
145
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { reading: 'no_answer', served_version: null, behind: false }) });
146
+ await b.settle();
147
+ const html = b.panel();
148
+ assert.match(html, /unknown rather than unchanged/);
149
+ assert.doesNotMatch(html, /id="dp-preview-1"/, 'and is offered no move');
150
+ });
151
+
152
+ test('an unenrolled control plane still lets the projects through', async () => {
153
+ // The regression this pins: the platform's own missing row used to be the whole
154
+ // page, so one un-run setup command would hide every project too.
155
+ const b = boot({ controlPlaneStatus: 404 });
156
+ await b.settle();
157
+ const html = b.panel();
158
+ assert.match(html, /has no deploy door yet/, 'the server’s own sentence, naming the fix');
159
+ assert.match(html, /mercury/, 'and the projects are still listed');
160
+ });
161
+
162
+ test('a viewer without the fleet atom falls back to their own projects', async () => {
163
+ const b = boot({ fleetStatus: 403 });
164
+ await b.settle();
165
+ assert.match(b.panel(), /mercury/);
166
+ });
167
+
168
+ test('a move re-reads ONLY the box it moved, not every project again', async () => {
169
+ // The grader's finding on the first round: refreshing one row by re-running the
170
+ // whole page load fans a probe out at every hosted project, including ones the
171
+ // owner never touched.
172
+ const b = boot();
173
+ await b.settle();
174
+ b.gets.length = 0;
175
+ b.els.get('dp-preview-32')._on.click();
176
+ b.els.get('dp-confirm-32')._on.click();
177
+ await b.settle();
178
+ const previewReads = b.gets.filter((g) => g.endsWith('/core-upgrade'));
179
+ assert.deepEqual(previewReads, ['/provisioning/instances/32/core-upgrade'],
180
+ 'exactly one preview re-read, and it is the moved box');
181
+ assert.ok(!b.gets.includes('/provisioning/fleet'), 'the fleet is not re-listed');
182
+ });
183
+
184
+ test('a move on the platform row re-reads the control plane, not a tenant preview', async () => {
185
+ const b = boot();
186
+ await b.settle();
187
+ b.gets.length = 0;
188
+ b.els.get('dp-preview-1')._on.click();
189
+ b.els.get('dp-confirm-1')._on.click();
190
+ await b.settle();
191
+ assert.ok(b.gets.includes('/provisioning/control-plane'), 'the self row is read from its own endpoint');
192
+ assert.ok(!b.gets.some((g) => /instances\/3\d\/core-upgrade/.test(g)), 'and no tenant is re-probed');
193
+ });
@@ -419,6 +419,37 @@ t('the publish workflow publishes PUBLIC, matching publishArgs', () => {
419
419
  );
420
420
  });
421
421
 
422
+ // THE CREDENTIAL, NOT THE CALLER (task 1003262). Migrating the lane to OIDC is only half
423
+ // of "delete NPM_TOKEN" — the half a future edit can quietly undo. If either token env is
424
+ // ever SET here, npm takes the legacy path and never attempts the OIDC exchange, so the
425
+ // migration would read as done while depending on a long-lived secret again. Asserted on
426
+ // ASSIGNMENTS, not on the string: the workflow names NPM_TOKEN three times on purpose, in
427
+ // comments that say there deliberately is none, and a naive grep would forbid saying so.
428
+ t('the publish lane sets no long-lived npm credential (OIDC only)', () => {
429
+ const yml = readFileSync(new URL('../.github/workflows/publish.yml', import.meta.url), 'utf8');
430
+ const uncommented = yml
431
+ .split('\n')
432
+ .filter((line) => !/^\s*#/.test(line))
433
+ .join('\n');
434
+
435
+ for (const name of ['NPM_TOKEN', 'NODE_AUTH_TOKEN']) {
436
+ assert.ok(
437
+ !new RegExp(`^\\s*${name}\\s*:`, 'm').test(uncommented),
438
+ `${name} must never be assigned in publish.yml — setting it sends npm down the legacy `
439
+ + 'token path and silently skips the OIDC exchange (ADR 0180 decision 4)',
440
+ );
441
+ }
442
+ assert.ok(
443
+ !/_authToken/.test(uncommented),
444
+ 'no .npmrc auth line may reappear in the lane',
445
+ );
446
+ assert.ok(
447
+ /id-token:\s*write/.test(uncommented),
448
+ 'publish.yml must request id-token: write — without it the runner mints no OIDC token '
449
+ + 'and npm publishes unauthenticated, failing with a misleading 404',
450
+ );
451
+ });
452
+
422
453
  t('buildPackageJson: omits `overrides` when the source has none', () => {
423
454
  const pkg = buildPackageJson({ version: '1.0.0', sourceDeps: { express: '^4.0.0', pg: '^8.0.0', pino: '^9.0.0' } });
424
455
  assert.equal(pkg.overrides, undefined);