@bongos/core 1.21.105 → 1.21.106

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.
Files changed (36) hide show
  1. package/.bongos-core.json +53 -43
  2. package/docs/copy-inventory.md +368 -366
  3. package/docs/copy-registry.json +475 -457
  4. package/docs/design/project-startup-direction.md +14 -0
  5. package/docs/module-api-changelog.md +2 -0
  6. package/docs/page-inventory.json +10 -3
  7. package/docs/page-readings.json +1023 -977
  8. package/modules/public-landing/public/projects-hall-live.states.json +3 -2
  9. package/modules/public-landing/public/projects-setup.states.json +91 -0
  10. package/modules/public-landing/public/projects.html +535 -804
  11. package/modules/public-landing/public/projects.probes.json +3 -3
  12. package/modules/public-landing/public/projects.states.json +1 -1
  13. package/modules/ui-design/kit/fixtures/provisioning-instances-hall-live.json +22 -2
  14. package/modules/ui-design/kit/fixtures/provisioning-instances-setup.json +195 -0
  15. package/modules/ui-design/kit/serve.js +22 -4
  16. package/package-lock.json +2 -2
  17. package/package.json +1 -1
  18. package/release-notes.json +6 -0
  19. package/src/module-api.js +1 -1
  20. package/tests/cosmos_token_contrast.mjs +10 -18
  21. package/tests/github_handback_any_device.mjs +4 -3
  22. package/tests/github_one_trip.mjs +45 -47
  23. package/tests/github_org_account.mjs +2 -2
  24. package/tests/github_pick_up.mjs +40 -29
  25. package/tests/onboard.mjs +13 -7
  26. package/tests/project_invite_ui.mjs +6 -3
  27. package/tests/project_modules_ui.mjs +3 -3
  28. package/tests/projects_hub.mjs +8 -9
  29. package/tests/projects_hub_cloud_hop.mjs +77 -125
  30. package/tests/projects_hub_dns_ready.mjs +42 -51
  31. package/tests/projects_hub_pre_uat.mjs +68 -69
  32. package/tests/public_landing_projects.mjs +45 -102
  33. package/tests/setup_status_facts.mjs +400 -0
  34. package/tests/wizard_github_feedback.mjs +90 -87
  35. package/tests/wizard_one_page.mjs +11 -9
  36. package/tests/wizard_auto_open.mjs +0 -94
@@ -167,12 +167,12 @@
167
167
  { "name": "part 02 (task 1004648): Change opens a row in place and reads Done, opening another closes it, and nothing needs confirming", "auth": true, "width": 1440, "url": "/projects?view=new",
168
168
  "steps": [["wait",600],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["wait",300],["click","#reviewRows .setRow[data-row=\"team\"] [data-row-chg]"],["wait",200],["click","[data-phys-solo=\"many\"]"],["wait",600],["click","#reviewRows .setRow[data-row=\"run\"] [data-row-chg]"],["wait",300]],
169
169
  "expect": {"visible":["#reviewRows .setRow[data-row=\"rewards\"]","#reviewRows .setRow.open[data-row=\"run\"] .opts"],"hidden":["#reviewRows .setRow[data-row=\"team\"] .opts"],"text":[["#reviewRows .setRow[data-row=\"run\"] [data-row-chg]","Done"],["#reviewRows .setRow[data-row=\"team\"] [data-row-chg]","Change"]]} },
170
- { "name": "the wizard driver (task 1004648): part 01, the checks, the address row (the dotless stub host takes a domain the founder owns), the tick, then Create — part 03 goes live with the checklist, the cloud hop and its status line", "auth": true, "width": 1440, "url": "/projects?view=new",
170
+ { "name": "the wizard driver (task 1004648): part 01, the checks, the address row (the dotless stub host takes a domain the founder owns), the tick, then Create — part 03 goes live as Getting <name> ready: one sentence, the checked facts, and the cloud hop behind its link", "auth": true, "width": 1440, "url": "/projects?view=new",
171
171
  "steps": [["wait",600],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["wait",300],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["click","#termsAck"],["wait",100],["click","#doCreate"],["wait",1500]],
172
- "expect": {"visible":["#panel-done","#humanSteps","#hop","#hopCanvas","#hopStatus","#hopJump"],"hidden":["#wizParts","#liveProgress"],"text":[["#done-h","Setting up Mercury"],["#hopStatus","Raising the server, database, and address — nothing needed from you."]]} },
172
+ "expect": {"visible":["#panel-done","#setupNow","#homeFacts","#ghFacts","#hopLink"],"hidden":["#wizParts","#hop","#doneAlive"],"text":[["#donePartT","Getting Mercury ready"]]} },
173
173
  { "name": "the wizard driver at 320 (narrower than a 375 phone): the page stacks, nothing scrolls sideways, and the walk still reaches part 03", "auth": true, "width": 320, "url": "/projects?view=new",
174
174
  "steps": [["wait",600],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["wait",300],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["click","#termsAck"],["wait",100],["click","#doCreate"],["wait",1500]],
175
- "expect": {"noOverflow":true,"visible":["#panel-done","#humanSteps"],"hidden":["#wizParts"]} },
175
+ "expect": {"noOverflow":true,"visible":["#panel-done","#homeFacts","#ghFacts"],"hidden":["#wizParts"]} },
176
176
  { "name": "invite a guild (task 1002303, R27): a /g/ address gives one pending line per member on its public page, each a link to the hall", "auth": true, "width": 1440, "url": "/projects?view=manage&id=101",
177
177
  "steps": [["wait",900],["fill","#mInviteGuild","/g/forge-crew"],["click","#mInvite [data-invite-guild]"],["wait",400]],
178
178
  "expect": { "count": [["#mInviteList li", 4]], "value": [["#mInviteGuild", ""]],
@@ -56,7 +56,7 @@
56
56
  "in-new-look": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["wait",300],["click","#reviewRows .setRow[data-row=\"look\"] [data-row-chg]"],["wait",400]],"expect":{"visible":["#reviewRows .setRow.open[data-row=\"look\"] .opts","#wizLook"]}},
57
57
  "in-new-look-custom": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["wait",300],["click","#reviewRows .setRow[data-row=\"look\"] [data-row-chg]"],["wait",400],["click","#wizLook-custom"],["wait",200]],"expect":{"visible":["#reviewRows .setRow.open[data-row=\"look\"] .opts","#wizLook","#wizAccent","#wizAccentNote"]}},
58
58
  "in-new-create-blocked": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#termsAck"],["wait",100],["click","#doCreate"],["wait",400]],"expect":{"visible":["#reviewRows .setRow.open[data-row=\"address\"] .opts","#h-domain"],"hidden":["#doneBody"],"text":[["#createWhy","Fix the row that’s open, then create."]]}},
59
- "in-new-done": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["wait",300],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["click","#termsAck"],["wait",100],["click","#doCreate"],["wait",1500]],"expect":{"visible":["#panel-done","#doneBody","#humanSteps","#hop","#hopCanvas","#hopStatus","#hopJump","#wizRecap"],"hidden":["#wizParts","#qWrap","#wizInvite","#liveProgress","#wizInviteLater"],"attr":[["#doCreate","disabled",""],["#wizRoom","data-screen","done"]],"text":[["#done-h","Setting up Mercury"],["#doCreate","Created"]]}},
59
+ "in-new-done": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["wait",300],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["click","#termsAck"],["wait",100],["click","#doCreate"],["wait",1500]],"expect":{"visible":["#panel-done","#doneBody","#setupNow","#homeFacts","#ghFacts","#hopLink","#wizRecap"],"hidden":["#wizParts","#qWrap","#wizInvite","#hop","#doneAlive"],"attr":[["#doCreate","disabled",""],["#wizRoom","data-screen","done"]],"text":[["#donePartT","Getting Mercury ready"],["#doCreate","Created"]]}},
60
60
  "in-new-resume-offer": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["wait",300],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["click","#termsAck"],["wait",100],["click","#doCreate"],["wait",1500],["click","#projTrig"],["wait",350],["click","#menuProjects [data-nav=\"new\"]"],["wait",500]],"expect":{"visible":["#panel-page","#wizResume","#wizResumeGo","#wizParts"],"hidden":["#doneBody"],"text":[["#wizResumeName","Mercury"]]}},
61
61
  "out-new": {"auth":false,"url":"/projects?view=new","expect":{"visible":["#panel-gate","#wizSignin"],"hidden":["#qWrap","#panel-page"]}},
62
62
  "out-map-projects": { "auth": false, "actions": [["click", "#projTrig"], ["wait", 350]], "expect": { "expanded": true, "hidden": ["#menuProjects [data-nav=\"mine\"]"] } },
@@ -1,5 +1,5 @@
1
1
  {
2
- "_": "The owner's projects for the kit (task 1003345): four states the mine list and the manage page render — live, standing up, snagged, torn down. Timestamps written \"$now-3m\" / \"$now-1d\" are resolved by the stub at request time so \"checked 3m ago\" stays true. Mooring (task 1003345, the grade's finding): live but unreachable since its last probe, so the restart door renders and the restart probe has a row to drive.",
2
+ "_": "The owner's projects for the kit (task 1003345): four states the mine list and the manage page render — live, standing up, snagged, torn down. Timestamps written \"$now-3m\" / \"$now-1d\" are resolved by the stub at request time so \"checked 3m ago\" stays true. Mooring (task 1003345, the grade's finding): live but unreachable since its last probe, so the restart door renders and the restart probe has a row to drive. Mercury (102) also carries the owner read's GitHub halves (github_app, github_setup), produced by the REAL projections with the app approved, installed and signed in, so part 03 reaches every required fact done (task 1004683).",
3
3
  "instances": [
4
4
  {
5
5
  "id": 101,
@@ -44,7 +44,27 @@
44
44
  "state": "running",
45
45
  "created_at": "$now-6m"
46
46
  },
47
- "dns_resolves": true
47
+ "dns_resolves": true,
48
+ "github_app": {
49
+ "status": "done",
50
+ "app_slug": "mercury-cloudbongos-com",
51
+ "failure": null,
52
+ "started_at": "2026-10-09T12:52:53.145Z"
53
+ },
54
+ "github_setup": {
55
+ "checklist": {
56
+ "approve": "done",
57
+ "install": "done",
58
+ "sign_in": "done",
59
+ "device_flow": "later",
60
+ "connected": true
61
+ },
62
+ "install_url": null,
63
+ "repo": "builder/mercury",
64
+ "verifying": true,
65
+ "pick_up": null,
66
+ "app_org": null
67
+ }
48
68
  },
49
69
  {
50
70
  "id": 103,
@@ -0,0 +1,195 @@
1
+ {
2
+ "_": "Part 03 after Create, one project per scenario of the design of record docs/design/mocks/day-night-rooms/setup-status.html (task 1004683): 201 just created, 202 waiting on GitHub, 203 installing, 204 stopped (the install never confirmed, the checking window over), 205 all done. Each row carries the github_app and github_setup halves of the owner's instance read, produced by the REAL projections (manifest-truth publicGithubApp, github-setup githubSetupRead); serve.js hands them out beside the instance on the single read. Regenerate the GitHub halves rather than hand-editing them.",
3
+ "instances": [
4
+ {
5
+ "id": 201,
6
+ "slug": "kestrel",
7
+ "domain": "kestrel.cloudbongos.com",
8
+ "target_ref": "founder/kestrel",
9
+ "hosting_shape": "cloud-host",
10
+ "onboard_mode": "greenfield",
11
+ "error_note": null,
12
+ "dns_note": null,
13
+ "torn_down_at": null,
14
+ "open_intent": null,
15
+ "status": "provisioning",
16
+ "created_at": "$now-1m",
17
+ "provisioned_at": null,
18
+ "updated_at": "$now-1m",
19
+ "last_verified_at": null,
20
+ "last_verified_up": null,
21
+ "dns_resolves": false,
22
+ "github_app": null,
23
+ "github_setup": {
24
+ "checklist": {
25
+ "approve": "todo",
26
+ "install": "todo",
27
+ "sign_in": "todo",
28
+ "device_flow": "todo",
29
+ "connected": false
30
+ },
31
+ "install_url": null,
32
+ "repo": "founder/kestrel",
33
+ "verifying": false,
34
+ "app_org": null,
35
+ "pick_up": null
36
+ }
37
+ },
38
+ {
39
+ "id": 202,
40
+ "slug": "heron",
41
+ "domain": "heron.cloudbongos.com",
42
+ "target_ref": "founder/heron",
43
+ "hosting_shape": "cloud-host",
44
+ "onboard_mode": "greenfield",
45
+ "error_note": null,
46
+ "dns_note": null,
47
+ "torn_down_at": null,
48
+ "open_intent": null,
49
+ "status": "active",
50
+ "created_at": "$now-9m",
51
+ "provisioned_at": "$now-6m",
52
+ "updated_at": "$now-6m",
53
+ "last_verified_at": null,
54
+ "last_verified_up": null,
55
+ "dns_resolves": false,
56
+ "github_app": {
57
+ "status": "pending",
58
+ "app_slug": null,
59
+ "failure": null,
60
+ "started_at": "2026-10-09T12:48:57.326Z"
61
+ },
62
+ "github_setup": {
63
+ "checklist": {
64
+ "approve": "now",
65
+ "install": "todo",
66
+ "sign_in": "todo",
67
+ "device_flow": "todo",
68
+ "connected": false
69
+ },
70
+ "install_url": null,
71
+ "repo": "founder/heron",
72
+ "verifying": false,
73
+ "pick_up": "reopen",
74
+ "app_org": null
75
+ }
76
+ },
77
+ {
78
+ "id": 203,
79
+ "slug": "plover",
80
+ "domain": "plover.cloudbongos.com",
81
+ "target_ref": "founder/plover",
82
+ "hosting_shape": "cloud-host",
83
+ "onboard_mode": "greenfield",
84
+ "error_note": null,
85
+ "dns_note": null,
86
+ "torn_down_at": null,
87
+ "open_intent": null,
88
+ "status": "active",
89
+ "created_at": "$now-14m",
90
+ "provisioned_at": "$now-11m",
91
+ "updated_at": "$now-11m",
92
+ "last_verified_at": "$now-9s",
93
+ "last_verified_up": true,
94
+ "dns_resolves": true,
95
+ "github_app": {
96
+ "status": "converting",
97
+ "app_slug": "plover-cloudbongos-com",
98
+ "failure": null,
99
+ "started_at": "2026-10-09T12:45:57.326Z"
100
+ },
101
+ "github_setup": {
102
+ "checklist": {
103
+ "approve": "done",
104
+ "install": "now",
105
+ "sign_in": "now",
106
+ "device_flow": "todo",
107
+ "connected": false
108
+ },
109
+ "install_url": "https://github.com/apps/plover-cloudbongos-com/installations/new",
110
+ "repo": "founder/plover",
111
+ "verifying": true,
112
+ "pick_up": null,
113
+ "app_org": null
114
+ }
115
+ },
116
+ {
117
+ "id": 204,
118
+ "slug": "tern",
119
+ "domain": "tern.cloudbongos.com",
120
+ "target_ref": "founder/tern",
121
+ "hosting_shape": "cloud-host",
122
+ "onboard_mode": "greenfield",
123
+ "error_note": null,
124
+ "dns_note": null,
125
+ "torn_down_at": null,
126
+ "open_intent": null,
127
+ "status": "active",
128
+ "created_at": "$now-4h",
129
+ "provisioned_at": "$now-235m",
130
+ "updated_at": "$now-235m",
131
+ "last_verified_at": "$now-9s",
132
+ "last_verified_up": true,
133
+ "dns_resolves": true,
134
+ "github_app": {
135
+ "status": "done",
136
+ "app_slug": "tern-cloudbongos-com",
137
+ "failure": null,
138
+ "started_at": "2026-10-09T08:59:57.326Z"
139
+ },
140
+ "github_setup": {
141
+ "checklist": {
142
+ "approve": "done",
143
+ "install": "now",
144
+ "sign_in": "done",
145
+ "device_flow": "later",
146
+ "connected": false
147
+ },
148
+ "install_url": "https://github.com/apps/tern-cloudbongos-com/installations/new",
149
+ "repo": "founder/tern",
150
+ "verifying": false,
151
+ "pick_up": "check",
152
+ "app_org": null
153
+ }
154
+ },
155
+ {
156
+ "id": 205,
157
+ "slug": "wren",
158
+ "domain": "wren.cloudbongos.com",
159
+ "target_ref": "founder/wren",
160
+ "hosting_shape": "cloud-host",
161
+ "onboard_mode": "greenfield",
162
+ "error_note": null,
163
+ "dns_note": null,
164
+ "torn_down_at": null,
165
+ "open_intent": null,
166
+ "status": "active",
167
+ "created_at": "$now-30m",
168
+ "provisioned_at": "$now-27m",
169
+ "updated_at": "$now-27m",
170
+ "last_verified_at": "$now-3s",
171
+ "last_verified_up": true,
172
+ "dns_resolves": true,
173
+ "github_app": {
174
+ "status": "done",
175
+ "app_slug": "wren-cloudbongos-com",
176
+ "failure": null,
177
+ "started_at": "2026-10-09T12:25:57.326Z"
178
+ },
179
+ "github_setup": {
180
+ "checklist": {
181
+ "approve": "done",
182
+ "install": "done",
183
+ "sign_in": "done",
184
+ "device_flow": "later",
185
+ "connected": true
186
+ },
187
+ "install_url": null,
188
+ "repo": "founder/wren",
189
+ "verifying": true,
190
+ "pick_up": null,
191
+ "app_org": null
192
+ }
193
+ }
194
+ ]
195
+ }
@@ -309,12 +309,27 @@ function instances(res, id, settings) {
309
309
  if (FAIL.has(name)) return json(res, 500, { error: { code: 'stub_failure', message: `FAIL=${name}` } });
310
310
  const file = path.join(FIXTURES, (ALIAS[name] || name) + '.json');
311
311
  if (!fs.existsSync(file)) return json(res, 404, { error: { code: 'no_fixture', message: name } });
312
- const list = stamp((JSON.parse(fs.readFileSync(file, 'utf8')).instances || []).map(withAsk).map(withPlanet));
312
+ // a fixture row may carry the owner read's two GitHub halves (github_app, github_setup —
313
+ // the shapes modules/provisioning/routes/provisioning.js adds, task 1004683); they ride
314
+ // the single read beside the instance, never inside it, and never the list
315
+ const rows = stamp((JSON.parse(fs.readFileSync(file, 'utf8')).instances || []).map(withAsk).map(withPlanet));
316
+ const bare = (i) => { const { github_app, github_setup, ...rest } = i; return rest; };
317
+ const list = rows.map(bare);
313
318
  if (id === null) return json(res, 200, { instances: list });
314
- const inst = list.find((i) => String(i.id) === String(id));
315
- if (!inst) return json(res, 404, { error: { code: 'not_found', message: 'no such instance' } });
319
+ const row = rows.find((i) => String(i.id) === String(id));
320
+ if (!row) return json(res, 404, { error: { code: 'not_found', message: 'no such instance' } });
321
+ const inst = bare(row);
316
322
  if (settings) return json(res, 200, { settings: { platform_visibility: 'public', joinability: 'open', visibility: 'public' }, planet: PLANETS.get(String(id)) || NO_PLANET, options: { platform_visibility: ['public', 'gated'], joinability: ['open', 'apply', 'invite_only'], visibility: ['public', 'private', 'stealth'] } });
317
- return json(res, 200, { instance: inst, open_intent: inst.open_intent || null });
323
+ return json(res, 200, { instance: inst, open_intent: inst.open_intent || null, github_app: row.github_app || null, github_setup: row.github_setup || null });
324
+ }
325
+ // GET /provisioning/github-app/resume?id=N — the route's resume record (routes/github-setup.js
326
+ // resumeRecord) for a fixture project, so a render can open part 03 on it by URL alone
327
+ function resumeAnswer(res, id) {
328
+ const file = path.join(FIXTURES, (ALIAS['provisioning-instances'] || 'provisioning-instances') + '.json');
329
+ const rows = fs.existsSync(file) ? (JSON.parse(fs.readFileSync(file, 'utf8')).instances || []) : [];
330
+ const inst = id ? rows.find((i) => String(i.id) === String(id)) : null;
331
+ const repo = inst && /^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}$/.test(String(inst.target_ref || '')) ? inst.target_ref : null;
332
+ return json(res, 200, { ok: true, project: inst ? { id: inst.id, slug: inst.slug, domain: inst.domain || null, onboard_mode: inst.onboard_mode || 'greenfield', repo } : null });
318
333
  }
319
334
  // A project's art in the featured fixture may be root-relative (the surface's
320
335
  // own /assets/…, served by this stub) — the page accepts only an http(s) art
@@ -644,6 +659,9 @@ const server = http.createServer((req, res) => {
644
659
  if (/^\/provisioning\/instances\/\d+\/invite-suggestions$/.test(rest) && req.method === 'GET') {
645
660
  return AUTH ? json(res, 200, { suggested: [], seeking: null, reason: null }) : json(res, 401, UNAUTH);
646
661
  }
662
+ if (rest === '/provisioning/github-app/resume' && req.method === 'GET') {
663
+ return AUTH ? resumeAnswer(res, u.searchParams.get('id')) : json(res, 401, UNAUTH);
664
+ }
647
665
  const one = rest.match(/^\/provisioning\/instances\/(\d+)(\/settings)?$/);
648
666
  if (one && req.method === 'GET') return AUTH ? instances(res, one[1], !!one[2]) : json(res, 401, UNAUTH);
649
667
  // the hub's own cross-project surface (task 1003774): the requested/leave/
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.105",
3
+ "version": "1.21.106",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.105",
9
+ "version": "1.21.106",
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.105",
3
+ "version": "1.21.106",
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",
@@ -9476,5 +9476,11 @@
9476
9476
  "id": "1004666",
9477
9477
  "text": "A check that is still running is no longer reported as a failed one. Before, a project whose test service posts a \"still working\" status would see its change called broken and trigger a pointless repair attempt; now it is corr"
9478
9478
  }
9479
+ ],
9480
+ "1.21.106": [
9481
+ {
9482
+ "id": "1004683",
9483
+ "text": "After pressing Create, a founder now sees \"Getting <name> ready\" with a plain count (\"4 of 6 done\", or \"ready\" with a tick) and one sentence saying what is happening and whether it is their turn. Below it are two short lists,"
9484
+ }
9479
9485
  ]
9480
9486
  }
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.105'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.106'; // 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');
@@ -515,24 +515,16 @@ test('text is never dimmed by subtree opacity', () => {
515
515
  assert.deepEqual(offenders, [], `\n ${offenders.join('\n ')}\n`);
516
516
  });
517
517
 
518
- test('the .prog strip mirrors .strip: state marks are --sea-ink, ticks are --on-ok', () => {
519
- // /create's done panel renders the page-local .prog, NOT cosmos's .strip —
520
- // a re-point of one and not the other is exactly how the 1.74:1 dark tick
521
- // shipped the first time. State-mark rules (done fill/connector, now
522
- // ring/pulse, the tick) must use the derived tokens; raw --sea is only legal
523
- // as a tint ingredient (color-mix) or the idle -soft rail.
524
- const progRules = [...pageSrc.matchAll(/\.prog[^{}]*\{([^}]*)\}/g)];
525
- assert.ok(progRules.length >= 5, 'the .prog rules moved out of projects.html — update this test');
526
- const bad = [];
527
- for (const m of progRules) {
528
- const body = m[1];
529
- if (/(?:border(?:-\w+)?|background)\s*:\s*var\(\s*--sea\s*\)/.test(body)) {
530
- bad.push(`raw --sea state mark: ${m[0].slice(0, 80)}`);
531
- }
532
- if (/border-(?:left|bottom)\s*:[^;]*#fff\b/i.test(body)) {
533
- bad.push(`hardcoded #fff tick: ${m[0].slice(0, 80)}`);
534
- }
535
- }
518
+ test('part 03 draws no stage strip, and its fact marks use the status inks, never raw --sea or #fff (task 1004683)', () => {
519
+ // The done panel's page-local .prog strip (a mirror of cosmos's .strip) left with the
520
+ // stage dots: part 03 is one list of checked facts now. The mine and manage views keep
521
+ // cosmos's own .strip. A fact's mark carries its state in the --ok / --err / --accent
522
+ // inks (which the day room re-points through --wz-*); raw --sea or a literal white
523
+ // would be the 1.74:1 dark tick this test was first written for.
524
+ assert.equal([...pageSrc.matchAll(/\.prog[^{}]*\{([^}]*)\}/g)].length, 0, 'no .prog rules are left in projects.html');
525
+ const marks = [...pageSrc.matchAll(/\.setFact\.(done|bad|now|you) \.d\{([^}]*)\}/g)];
526
+ assert.ok(marks.length >= 4, 'the four marked fact states are found');
527
+ const bad = marks.filter((m) => /var\(\s*--sea\s*\)|#fff\b/i.test(m[2])).map((m) => m[0].slice(0, 80));
536
528
  assert.deepEqual(bad, [], `\n ${bad.join('\n ')}\n`);
537
529
  });
538
530
 
@@ -82,8 +82,8 @@ function region(startMark, endMark) {
82
82
  const e = s < 0 ? -1 : SCRIPT.indexOf(endMark, s);
83
83
  return s > 0 && e > s ? SCRIPT.slice(s, e) : null;
84
84
  }
85
- const POLLS = region(" /* ── C8: the wizard's polls run only while the create view is on stage", ' /* ── live build progress (task 2083)');
86
- const WATCH_VARS = region(' var pollTimer = null;', " /* The done panel's own flow vocabularies");
85
+ const POLLS = region(" /* ── C8: the wizard's polls run only while the create view is on stage", " /* ── the owner's instance read (task 2083)");
86
+ const WATCH_VARS = region(' var pollTimer = null;', ' function startProgress(id) {');
87
87
  const START_PROGRESS = (SCRIPT.match(/ {4}function startProgress\(id\) \{[\s\S]*?\n {4}\}/) || [null])[0];
88
88
  const DOORS = region(" /* ── the hub's three doors ── */", ' return { enter: enter, leave: leave, onAuth: onAuth };');
89
89
 
@@ -136,7 +136,8 @@ function page({ url, who, storage = 'empty', name = '', hidden = false, instance
136
136
  renderDone() { log.rendered += 1; }, renderSteps() { log.rendered += 1; },
137
137
  resetForNewProject() { log.resets += 1; },
138
138
  saveDraft() {},
139
- setProgress() {}, progressMarkup: () => '', setMoment() {}, renderProgress: () => false, addressReady: () => true,
139
+ // part 03's facts (task 1004683) are tests/setup_status_facts.mjs; here they only have to exist
140
+ paintFacts() {}, homeFacts: () => [], ghFacts: () => [], setupReady: () => false, hostOk: () => true, addressReady: () => true,
140
141
  };
141
142
  sandbox.globalThis = sandbox;
142
143
  vm.createContext(sandbox);
@@ -8,8 +8,8 @@
8
8
  // app's OWN key: the creds read back out of a REAL webEnvBody, a JWT that verifies,
9
9
  // installed / not installed / cannot tell, device flow on / off / cannot tell, and the
10
10
  // tick recording only what GitHub confirmed — never a secret in a log line.
11
- // 3. the REAL projection (githubSetupRead) run into the REAL page renderers (projects.html,
12
- // the vm-slice idiom): each server state draws the step the founder should see.
11
+ // 3. the REAL projection (githubSetupRead) run into the REAL page facts (projects.html,
12
+ // ghFacts, the vm-slice idiom; task 1004683): each server state draws the fact the founder should see.
13
13
  // 4. the device-flow finding is written down where the next builder will look.
14
14
  //
15
15
  // Run: node --test tests/github_one_trip.mjs
@@ -185,38 +185,35 @@ test('the window query takes each project\'s LATEST attempt, past placement, sti
185
185
  assert.match(fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'provision.js'), 'utf8'), /require\('\.\/provision-github-setup\.js'\)\.verifyGithubAppSetups\(deps\)/, 'it runs on every runner tick, after the conversions');
186
186
  });
187
187
 
188
- // ── 3. the real projection into the real renderers ──────────────────────────
188
+ // ── 3. the real projection into the real page facts ─────────────────────────
189
+ // Since task 1004683 part 03 draws FACTS, not checklist markup: ghFacts() is pure over
190
+ // what the reads said, and paintFacts draws it from nodes (tests/setup_status_facts.mjs
191
+ // runs the whole route-to-DOM path). Here: the GitHub half, fed by the real projection.
189
192
 
190
- const RS = SCRIPT.indexOf(' /* The four GitHub steps of part 03');
191
- const RE = SCRIPT.indexOf(' var STEP_RENDERERS = {', RS);
193
+ const FS = SCRIPT.indexOf(' function ghStep(key) {');
194
+ const FE = SCRIPT.indexOf(' /* ── drawing it: nodes and textContent only', FS);
192
195
  const SS = SCRIPT.indexOf(' /* the checklist the server derived (task 1004649)');
193
196
  const SE = SCRIPT.indexOf(' function pollManifest(stateToken) {', SS);
194
- const PS = SCRIPT.indexOf(' function stepsPaint() {');
195
- const PE = SCRIPT.indexOf(' (function hopInput()', PS);
196
197
 
197
198
  test('the regions are findable (a rename must fail loudly)', () => {
198
- assert.ok(RS > 0 && RE > RS && SS > 0 && SE > SS && PS > 0 && PE > PS);
199
+ assert.ok(FS > 0 && FE > FS && SS > 0 && SE > SS);
200
+ assert.match(SCRIPT.slice(FS, FE), /function ghFacts\(now\)/);
199
201
  });
200
202
 
201
203
  function page({ manifest, appSlug = null }) {
202
204
  const sandbox = {
203
- live: { manifest, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug, setup: null, installUrl: null, instStatus: 'active', addrReady: true },
205
+ live: { manifest, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug, appOrg: null, setup: null, installUrl: null, pickUp: null, verifying: false, askedPlatform: false, instStatus: 'active', addrReady: true, inst: null, readAt: 0, gone: '', repo: null },
204
206
  state: { name: 'Mer<cury', slug: 'mercury', owner: 'acme', repo: 'mercury', step: 2 },
205
- autoOpenArmed: false,
206
- resolvedDomain: () => 'mercury.example.com', apexBase: () => 'example.com',
207
- esc: (x) => String(x).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])),
208
- GH_ICON: '', manualOauthDetails: () => '', projectPageLink: (t) => t, autoOpenTick: () => '',
209
- progressMarkup: (a, b, note) => `<prog>${note}</prog>`, encodeURIComponent, JSON, Array,
207
+ resolvedDomain: () => 'mercury.example.com', apexBase: () => 'example.com', addressReady: () => true,
208
+ document: { hidden: false, hasFocus: () => true },
210
209
  };
211
210
  vm.createContext(sandbox);
212
- vm.runInContext(`${SCRIPT.slice(RS, RE)}\n${SCRIPT.slice(SS, SE)}\n${SCRIPT.slice(PS, PE)}`, sandbox);
211
+ vm.runInContext(`${SCRIPT.slice(FS, FE)}\n${SCRIPT.slice(SS, SE)}`, sandbox);
213
212
  return sandbox;
214
213
  }
215
- const ROWS = ['renderStepOauthApp', 'renderStepAppInstall', 'renderStepFirstSignIn', 'renderStepDeviceFlow'];
216
- function draw(sb) {
217
- return ROWS.map((f) => vm.runInContext(`${f}()`, sb));
218
- }
219
- function cls(li) { return ((li.match(/^<li(?: class="([^"]*)")?/) || [])[1] || '').split(/\s+/).filter(Boolean); }
214
+ // vm-born values compared through JSON
215
+ const draw = (sb) => JSON.parse(JSON.stringify(vm.runInContext('ghFacts(Date.now())', sb)));
216
+ const states = (rows) => rows.map((r) => r.state);
220
217
 
221
218
  async function serverSays(status, appSlug, eventRows) {
222
219
  const db = { query: async () => ({ rows: eventRows }) };
@@ -224,55 +221,50 @@ async function serverSays(status, appSlug, eventRows) {
224
221
  return GS.githubSetupRead(db, { target_ref: 'acme/mercury' }, row, status);
225
222
  }
226
223
 
227
- test('the mockup\'s four steps, in its words and order, the typed name escaped', () => {
228
- const sb = page({ manifest: null });
229
- const rows = draw(sb);
230
- const titles = rows.map((li) => (li.match(/<b>([^<]*)<\/b>/) || [])[1]);
231
- assert.deepEqual(titles, ['Approve the Mer&lt;cury app on GitHub', 'Install it on your repo', 'Builders can sign in', 'Command-line sign-in is on']);
232
- assert.match(rows[0], /data-action="manifest-start"[^>]*>Connect GitHub ↗/, 'one click starts the one trip');
233
- assert.equal(SCRIPT.match(/var STEP_ORDER = \[([^\]]*)\]/)[1], "'github-repo', 'oauth-app', 'app-install', 'first-sign-in', 'device-flow', 'enter-hall'", 'drawn in the mockup\'s order');
224
+ test('the mockup\'s four facts, in its words and order; the typed name is text, never markup', () => {
225
+ const rows = draw(page({ manifest: null }));
226
+ assert.deepEqual(rows.map((r) => r.title), ['Approve the Mer<cury app on GitHub', 'Install it on your repo', 'Builders can sign in', 'Command-line sign-in']);
227
+ assert.deepEqual(rows.map((r) => r.key), ['approve', 'install', 'sign_in', 'device_flow']);
228
+ assert.equal(rows[0].act, 'connect', 'one click starts the one trip, from inside the Approve fact');
229
+ assert.equal(rows[0].state, 'you', 'it is the founder\'s turn');
230
+ assert.equal(rows[3].opt, true, 'command-line sign-in is optional');
231
+ assert.doesNotMatch(SCRIPT.slice(FS, FE), /esc\(|innerHTML/, 'the facts carry text; the painter writes nodes');
234
232
  });
235
233
 
236
- test('each server state draws the step the founder should see — from the REAL projection', async () => {
234
+ test('each server state draws the facts the founder should see — from the REAL projection', async () => {
237
235
  // keys placed, nothing verified on GitHub yet
238
236
  let sb = page({ manifest: 'converting', appSlug: 'mercury-app' });
239
237
  let d = await serverSays('converting', 'mercury-app', []);
240
- vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)})`, sb);
238
+ vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)}, ${JSON.stringify(d)})`, sb);
241
239
  let rows = draw(sb);
242
- assert.deepEqual(rows.map(cls), [['sdone'], [], ['slocked'], ['slocked', 'soptional']]);
243
- assert.match(rows[1], /href="https:\/\/github\.com\/apps\/mercury-app\/installations\/new\?state=S1"/, 'the way back to the install, with the attempt');
244
- assert.equal(vm.runInContext('stepsPaint.toString().length > 0', sb), true);
240
+ assert.deepEqual(states(rows), ['done', 'you', 'now', 'todo']);
241
+ assert.equal(rows[1].href, 'https://github.com/apps/mercury-app/installations/new?state=S1', 'the way back to the install, with the attempt');
242
+ assert.equal(rows[1].sub, 'acme/mercury', 'the repo the server says it installs on');
243
+ assert.equal(rows[1].right, 'not yet seen');
245
244
 
246
245
  // sign-in verified, install still unconfirmed: NOT connected
247
246
  sb = page({ manifest: 'done', appSlug: 'mercury-app' });
248
247
  d = await serverSays('done', 'mercury-app', []);
249
- vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)})`, sb);
248
+ vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)}, ${JSON.stringify(d)})`, sb);
250
249
  rows = draw(sb);
251
- assert.deepEqual(rows.map(cls), [['sdone'], [], ['sdone'], ['soptional']]);
252
- assert.match(rows[3], /Optional — turn it on later\./);
250
+ assert.deepEqual(states(rows), ['done', 'you', 'done', 'todo']);
251
+ assert.equal(JSON.stringify(rows[3].sub), JSON.stringify(['Turn it on later in ', { t: 'the app settings', href: 'https://github.com/settings/apps/mercury-app' }, '.']));
253
252
  assert.equal(vm.runInContext('setupOpen()', sb), true, 'the page keeps watching for the install');
254
253
 
255
254
  // GitHub confirmed the install to the runner: connected, device flow still optional
256
255
  sb = page({ manifest: 'done', appSlug: 'mercury-app' });
257
256
  d = await serverSays('done', 'mercury-app', [{ event: 'github-app-installed', at: '2026-10-08T10:00:00Z' }]);
258
- vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)})`, sb);
257
+ vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)}, ${JSON.stringify(d)})`, sb);
259
258
  rows = draw(sb);
260
- assert.deepEqual(rows.map(cls), [['sdone'], ['sdone'], ['sdone'], ['soptional']]);
261
- assert.match(rows[1], /The app is on acme\/mercury/);
259
+ assert.deepEqual(states(rows), ['done', 'done', 'done', 'todo']);
262
260
  assert.equal(vm.runInContext('setupOpen()', sb), false);
263
- // the part's state line, from these very rows: the optional step does not hold it back
264
- const items = rows.map((li) => ({ classList: { contains: (c) => cls(li).includes(c) } }));
265
- const ghState = { textContent: '' };
266
- sb.$ = (id) => (id === 'humanSteps' ? { children: items } : id === 'ghState' ? ghState : null);
267
- sb.manifestInFlight = () => false;
268
- vm.runInContext('stepsPaint()', sb);
269
- assert.equal(ghState.textContent, 'connected ✓');
261
+ assert.equal(vm.runInContext('setupCount([], ghFacts(Date.now()))', sb), 'ready ✓', 'the optional fact does not hold the count back');
270
262
 
271
263
  // and once GitHub has device flow on, the fourth ticks too
272
264
  sb = page({ manifest: 'done', appSlug: 'mercury-app' });
273
265
  d = await serverSays('done', 'mercury-app', [{ event: 'github-app-installed', at: '2026-10-08T10:00:00Z' }, { event: 'github-app-device-flow', at: '2026-10-08T10:05:00Z' }]);
274
- vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)})`, sb);
275
- assert.deepEqual(cls(draw(sb)[3]), ['sdone', 'soptional']);
266
+ vm.runInContext(`seedSetup(${JSON.stringify(d.checklist)}, ${JSON.stringify(d.install_url)}, ${JSON.stringify(d)})`, sb);
267
+ assert.equal(draw(sb)[3].state, 'done');
276
268
  });
277
269
 
278
270
  test('seedSetup is change-only, and an install link must be GitHub\'s', () => {
@@ -286,6 +278,12 @@ test('seedSetup is change-only, and an install link must be GitHub\'s', () => {
286
278
  vm.runInContext(`seedSetup(${c}, 'javascript:alert(1)')`, sb2);
287
279
  assert.equal(vm.runInContext('live.installUrl', sb2), null);
288
280
  assert.equal(vm.runInContext('seedSetup(null, null)', sb2), false);
281
+ // the repo it installs on rides the read, strict
282
+ const sb3 = page({ manifest: 'done', appSlug: 'a' });
283
+ vm.runInContext(`seedSetup(${c}, null, { repo: 'acme/mer cury' })`, sb3);
284
+ assert.equal(vm.runInContext('live.repo', sb3), null, 'a malformed repo never reaches the page');
285
+ vm.runInContext(`seedSetup(${c}, null, { repo: 'acme/mercury' })`, sb3);
286
+ assert.equal(vm.runInContext('live.repo', sb3), 'acme/mercury');
289
287
  });
290
288
 
291
289
  // ── 4. the finding is written down ──────────────────────────────────────────
@@ -113,7 +113,7 @@ const PICKER = region(" /* ── whose GitHub account: mine or an organisati
113
113
  const ENSURE_REPO = region(' function ensureFreshRepo() {', " $('doCreate').addEventListener('click'");
114
114
  const NOTE_ORG = region(" /* the organisation the server says this attempt's app is on: change-only */", ' function startManifest(btn) {');
115
115
  const START = region(' function startManifest(btn) {', ' /* the "start again" offer once a wait has clearly gone wrong');
116
- const SETTINGS = region(' /* the organisation that owns the app, when the founder made it on one (task 1004651) */', ' /* oauth-app — the ADR 0133 one-click GitHub-App manifest flow');
116
+ const SETTINGS = region(' /* the organisation that owns the app, when the founder made it on one (task 1004651) */', ' /* When a fact was seen, said plainly');
117
117
 
118
118
  test('the page regions are still findable (a rename must fail loudly, not test nothing)', () => {
119
119
  for (const [name, r] of Object.entries({ PICKER, ENSURE_REPO, NOTE_ORG, START, SETTINGS })) assert.ok(r, `${name} region not found`);
@@ -165,7 +165,7 @@ function page({ state = {}, live = {} } = {}) {
165
165
  live: { manifest: null, manifestMsg: '', manifestFailure: null, appSlug: null, appOrg: null, setup: null, installUrl: null, ...live },
166
166
  repoAccess: true, instanceId: 42, me: { github_login: 'founder' },
167
167
  hopForRepoAccess: () => false, syncHome() {}, saveDraft() {},
168
- rememberManifest() {}, renderSteps() {}, pollManifest() {}, watchGhTab() {},
168
+ rememberManifest() {}, paintFacts() {}, repaintFacts() {}, pollManifest() {}, watchGhTab() {},
169
169
  };
170
170
  sandbox.manifestStartedAt = 0;
171
171
  vm.createContext(sandbox);