@bongos/core 1.20.83 → 1.21.1
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 +151 -61
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +7 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0051-full-session-transcript-corpus.md +3 -1
- package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +3 -3
- package/docs/adr/README.md +1 -1
- package/docs/api/openapi.json +153 -4
- package/docs/api-reference.md +5 -3
- package/docs/architecture.md +3 -0
- package/docs/copy-inventory.md +10 -10
- package/docs/copy-registry.json +12 -12
- package/docs/design/project-startup-direction.md +63 -0
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +6 -0
- package/docs/page-inventory.json +8 -3
- package/docs/page-readings.json +382 -373
- package/docs/recipes/private-npm-distribution.md +1 -1
- package/migrations/core_269_session_transcripts.sql +35 -0
- package/modules/hall-ui/public/genesis-home.js +17 -3
- package/modules/hall-ui/public/index-genesis-demo.states.json +13 -0
- package/modules/lifecycle/workflow-dispatch.js +21 -11
- package/modules/npm-release/release.js +10 -1
- package/modules/onboarding/founding-birth.js +116 -0
- package/modules/onboarding/genesis-home.js +72 -5
- package/modules/onboarding/port.js +9 -2
- package/modules/onboarding/routes/onboarding.js +8 -1
- package/modules/provisioning/birth.js +64 -0
- package/modules/provisioning/dns-resolves.js +54 -0
- package/modules/provisioning/migrations/provisioning_036_dns_resolved.sql +30 -0
- package/modules/provisioning/pollers/liveness-sweep.js +5 -0
- package/modules/provisioning/provisioning.js +7 -6
- package/modules/provisioning/routes/provisioning.js +6 -2
- package/modules/provisioning/tests/liveness.mjs +48 -0
- package/modules/public-landing/public/projects-dns-pending.states.json +85 -0
- package/modules/public-landing/public/projects.html +40 -15
- package/modules/sessions/db.js +58 -0
- package/modules/sessions/routes/sessions.js +97 -0
- package/modules/ui-design/kit/fixtures/founding-genesis-demo.json +176 -0
- package/modules/ui-design/kit/fixtures/me-founding-demo.json +27 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances-demo.json +3 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances-dns-pending.json +49 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances.json +3 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +35 -0
- package/scripts/gds/provision.js +77 -53
- package/scripts/gds/session-digest.js +11 -3
- package/scripts/gds/session-transcript-build.js +155 -0
- package/scripts/gds/session-transcript-upload.js +29 -0
- package/scripts/gds/ship-session-upload.js +6 -1
- package/scripts/gds/upgrade-commit-identity.js +32 -0
- package/scripts/gds/upgrade.js +1 -1
- package/src/bongos/routes.js +3 -0
- package/src/branding.js +9 -0
- package/src/module-api.js +1 -1
- package/tests/founding_birth.mjs +200 -0
- package/tests/founding_mode.mjs +3 -1
- package/tests/genesis_home.mjs +71 -2
- package/tests/hub_map_orb_states.mjs +5 -1
- package/tests/npm_release_release.mjs +37 -2
- package/tests/projects_hub.mjs +5 -3
- package/tests/projects_hub_dns_ready.mjs +153 -0
- package/tests/projects_hub_pre_uat.mjs +9 -5
- package/tests/provision_dns_first.mjs +171 -0
- package/tests/provision_settings_apply.mjs +6 -1
- package/tests/provisioning_settings_env.mjs +49 -0
- package/tests/session_transcript.mjs +273 -0
- package/tests/ui_design_kit.mjs +1 -1
- package/tests/upgrade_commit_identity.mjs +89 -0
- package/tests/wizard_demo.mjs +29 -3
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "A demo born into genesis (task 1004506): 3 people, 4 hours each. The plan was settled at birth from the demo (Monarchy / BDFL, one pass, no advisors), so decide is behind and scope is in progress, each stage minutes long. Built from the real server shaper (genesis-home.js demoPlan + shapeGenesis).",
|
|
3
|
+
"genesis": {
|
|
4
|
+
"stage": "scope",
|
|
5
|
+
"stage_number": 1,
|
|
6
|
+
"stage_count": 5,
|
|
7
|
+
"day": 1,
|
|
8
|
+
"deciding": false,
|
|
9
|
+
"plan": {
|
|
10
|
+
"days": {
|
|
11
|
+
"scope": 1,
|
|
12
|
+
"strategize": 1,
|
|
13
|
+
"charter": 1,
|
|
14
|
+
"legalize": 1,
|
|
15
|
+
"legislation": 1
|
|
16
|
+
},
|
|
17
|
+
"passes": 1,
|
|
18
|
+
"advisors": "none",
|
|
19
|
+
"government": "monarchy",
|
|
20
|
+
"expected_builders": 3,
|
|
21
|
+
"existing": false,
|
|
22
|
+
"demo": {
|
|
23
|
+
"people": 3,
|
|
24
|
+
"hours_each": 4
|
|
25
|
+
},
|
|
26
|
+
"minutes": {
|
|
27
|
+
"scope": 14,
|
|
28
|
+
"strategize": 10,
|
|
29
|
+
"charter": 10,
|
|
30
|
+
"legalize": 5,
|
|
31
|
+
"legislation": 10
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"demo": {
|
|
35
|
+
"people": 3,
|
|
36
|
+
"hours_each": 4
|
|
37
|
+
},
|
|
38
|
+
"stages": [
|
|
39
|
+
{
|
|
40
|
+
"key": "scope",
|
|
41
|
+
"number": 1,
|
|
42
|
+
"label": "Scope",
|
|
43
|
+
"life": "primordial sea",
|
|
44
|
+
"plate": "r5-life-1-primordial-sea",
|
|
45
|
+
"done_when": "The scope is written: what you are making, for whom, and what is left out.",
|
|
46
|
+
"wakes": [
|
|
47
|
+
"roadmap",
|
|
48
|
+
"goals",
|
|
49
|
+
"goal-map"
|
|
50
|
+
],
|
|
51
|
+
"days": 1,
|
|
52
|
+
"minutes": 14,
|
|
53
|
+
"due": "2026-10-01T09:14:00.000Z",
|
|
54
|
+
"state": "now",
|
|
55
|
+
"tasks": [
|
|
56
|
+
{
|
|
57
|
+
"id": "101",
|
|
58
|
+
"title": "Write the project scope: what we are making, for whom, and what is out of scope",
|
|
59
|
+
"status": "active",
|
|
60
|
+
"finished": false
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"finished": 0,
|
|
64
|
+
"total": 1,
|
|
65
|
+
"board": null
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"key": "strategize",
|
|
69
|
+
"number": 2,
|
|
70
|
+
"label": "Strategize",
|
|
71
|
+
"life": "first cells",
|
|
72
|
+
"plate": "r5-life-2-first-cells",
|
|
73
|
+
"done_when": "The strategy is written out, or the one you have is linked.",
|
|
74
|
+
"wakes": [
|
|
75
|
+
"work"
|
|
76
|
+
],
|
|
77
|
+
"days": 1,
|
|
78
|
+
"minutes": 10,
|
|
79
|
+
"due": null,
|
|
80
|
+
"state": "later",
|
|
81
|
+
"tasks": [
|
|
82
|
+
{
|
|
83
|
+
"id": "102",
|
|
84
|
+
"title": "Write out the strategy, or link the one you already have",
|
|
85
|
+
"status": "ready",
|
|
86
|
+
"finished": false
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"finished": 0,
|
|
90
|
+
"total": 1,
|
|
91
|
+
"board": null
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"key": "charter",
|
|
95
|
+
"number": 3,
|
|
96
|
+
"label": "Charter",
|
|
97
|
+
"life": "the genome",
|
|
98
|
+
"plate": "r5-life-3-genome",
|
|
99
|
+
"done_when": "The starting government is chosen and the bylaws are written.",
|
|
100
|
+
"wakes": [
|
|
101
|
+
"board-room"
|
|
102
|
+
],
|
|
103
|
+
"days": 1,
|
|
104
|
+
"minutes": 10,
|
|
105
|
+
"due": null,
|
|
106
|
+
"state": "later",
|
|
107
|
+
"tasks": [
|
|
108
|
+
{
|
|
109
|
+
"id": "103",
|
|
110
|
+
"title": "Choose the starting government and write the bylaws",
|
|
111
|
+
"status": "ready",
|
|
112
|
+
"finished": false
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"finished": 0,
|
|
116
|
+
"total": 1,
|
|
117
|
+
"board": null
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"key": "legalize",
|
|
121
|
+
"number": 4,
|
|
122
|
+
"label": "Legalize",
|
|
123
|
+
"life": "the membrane",
|
|
124
|
+
"plate": "r5-life-4-membrane",
|
|
125
|
+
"done_when": "Rewards are decided, and who can be paid.",
|
|
126
|
+
"wakes": [
|
|
127
|
+
"economy"
|
|
128
|
+
],
|
|
129
|
+
"days": 1,
|
|
130
|
+
"minutes": 5,
|
|
131
|
+
"due": null,
|
|
132
|
+
"state": "later",
|
|
133
|
+
"tasks": [
|
|
134
|
+
{
|
|
135
|
+
"id": "104",
|
|
136
|
+
"title": "Decide how contributors are rewarded and who can be paid",
|
|
137
|
+
"status": "ready",
|
|
138
|
+
"finished": false
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
"finished": 0,
|
|
142
|
+
"total": 1,
|
|
143
|
+
"board": null
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"key": "legislation",
|
|
147
|
+
"number": 5,
|
|
148
|
+
"label": "Legislation",
|
|
149
|
+
"life": "the nervous system",
|
|
150
|
+
"plate": "r5-life-5-nervous-system",
|
|
151
|
+
"done_when": "The voting rules are set: who votes, and how many must agree.",
|
|
152
|
+
"wakes": [
|
|
153
|
+
"ranks"
|
|
154
|
+
],
|
|
155
|
+
"days": 1,
|
|
156
|
+
"minutes": 10,
|
|
157
|
+
"due": null,
|
|
158
|
+
"state": "later",
|
|
159
|
+
"tasks": [
|
|
160
|
+
{
|
|
161
|
+
"id": "105",
|
|
162
|
+
"title": "Set the voting rules: who votes and how many must agree",
|
|
163
|
+
"status": "ready",
|
|
164
|
+
"finished": false
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
"finished": 0,
|
|
168
|
+
"total": 1,
|
|
169
|
+
"board": null
|
|
170
|
+
}
|
|
171
|
+
],
|
|
172
|
+
"can_close": false,
|
|
173
|
+
"can_come_alive": false
|
|
174
|
+
},
|
|
175
|
+
"viewer_id": "1"
|
|
176
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"builder": {
|
|
3
|
+
"id": "1",
|
|
4
|
+
"github_login": "builder",
|
|
5
|
+
"display_name": "A Builder",
|
|
6
|
+
"avatar_url": "/__kit/avatar.svg",
|
|
7
|
+
"rank": "archon"
|
|
8
|
+
},
|
|
9
|
+
"founding_mode": {
|
|
10
|
+
"active": true,
|
|
11
|
+
"stage": "scope",
|
|
12
|
+
"stage_number": 1,
|
|
13
|
+
"stage_count": 5,
|
|
14
|
+
"day": 1,
|
|
15
|
+
"asleep": {
|
|
16
|
+
"roadmap": "scope",
|
|
17
|
+
"goals": "scope",
|
|
18
|
+
"goal-map": "scope",
|
|
19
|
+
"work": "strategize",
|
|
20
|
+
"board-room": "charter",
|
|
21
|
+
"economy": "legalize",
|
|
22
|
+
"ranks": "legislation",
|
|
23
|
+
"collab": "alive",
|
|
24
|
+
"roster": "alive"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"last_verified_up": true,
|
|
17
17
|
"error_note": null,
|
|
18
18
|
"dns_note": null,
|
|
19
|
+
"dns_resolves": true,
|
|
19
20
|
"torn_down_at": null,
|
|
20
21
|
"open_intent": null,
|
|
21
22
|
"demo": {
|
|
@@ -89,6 +90,7 @@
|
|
|
89
90
|
"last_verified_up": true,
|
|
90
91
|
"error_note": null,
|
|
91
92
|
"dns_note": null,
|
|
93
|
+
"dns_resolves": true,
|
|
92
94
|
"torn_down_at": "2026-08-15T12:00:00.000Z",
|
|
93
95
|
"open_intent": null
|
|
94
96
|
},
|
|
@@ -107,6 +109,7 @@
|
|
|
107
109
|
"last_verified_up": false,
|
|
108
110
|
"error_note": null,
|
|
109
111
|
"dns_note": null,
|
|
112
|
+
"dns_resolves": true,
|
|
110
113
|
"torn_down_at": null,
|
|
111
114
|
"open_intent": null
|
|
112
115
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "Two projects whose address the platform's own lookup has not seen answer yet (task 1004505): beacon is active a minute after its standup, lumen is still standing up. The hub must link neither and name neither until dns_resolves is true. Only projects-dns-pending.states.json points at this.",
|
|
3
|
+
"instances": [
|
|
4
|
+
{
|
|
5
|
+
"id": 106,
|
|
6
|
+
"slug": "beacon",
|
|
7
|
+
"status": "active",
|
|
8
|
+
"domain": "beacon.cloudbongos.com",
|
|
9
|
+
"target_ref": "builder/beacon",
|
|
10
|
+
"hosting_shape": "cloud-host",
|
|
11
|
+
"owner_monthly_usd": null,
|
|
12
|
+
"onboard_mode": "scaffold",
|
|
13
|
+
"created_at": "$now-5m",
|
|
14
|
+
"provisioned_at": "$now-1m",
|
|
15
|
+
"updated_at": "$now-1m",
|
|
16
|
+
"last_verified_at": null,
|
|
17
|
+
"last_verified_up": null,
|
|
18
|
+
"error_note": null,
|
|
19
|
+
"dns_note": null,
|
|
20
|
+
"dns_resolves": false,
|
|
21
|
+
"torn_down_at": null,
|
|
22
|
+
"open_intent": null
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": 107,
|
|
26
|
+
"slug": "lumen",
|
|
27
|
+
"status": "provisioning",
|
|
28
|
+
"domain": "lumen.cloudbongos.com",
|
|
29
|
+
"target_ref": "builder/lumen",
|
|
30
|
+
"hosting_shape": "cloud-host",
|
|
31
|
+
"owner_monthly_usd": null,
|
|
32
|
+
"onboard_mode": "scaffold",
|
|
33
|
+
"created_at": "$now-3m",
|
|
34
|
+
"provisioned_at": null,
|
|
35
|
+
"updated_at": "$now-2m",
|
|
36
|
+
"last_verified_at": null,
|
|
37
|
+
"last_verified_up": null,
|
|
38
|
+
"error_note": null,
|
|
39
|
+
"dns_note": null,
|
|
40
|
+
"dns_resolves": false,
|
|
41
|
+
"torn_down_at": null,
|
|
42
|
+
"open_intent": {
|
|
43
|
+
"action": "provision",
|
|
44
|
+
"state": "running",
|
|
45
|
+
"created_at": "$now-3m"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"last_verified_up": true,
|
|
17
17
|
"error_note": null,
|
|
18
18
|
"dns_note": null,
|
|
19
|
+
"dns_resolves": true,
|
|
19
20
|
"torn_down_at": null,
|
|
20
21
|
"open_intent": null
|
|
21
22
|
},
|
|
@@ -75,6 +76,7 @@
|
|
|
75
76
|
"last_verified_up": true,
|
|
76
77
|
"error_note": null,
|
|
77
78
|
"dns_note": null,
|
|
79
|
+
"dns_resolves": true,
|
|
78
80
|
"torn_down_at": "2026-08-15T12:00:00.000Z",
|
|
79
81
|
"open_intent": null
|
|
80
82
|
},
|
|
@@ -93,6 +95,7 @@
|
|
|
93
95
|
"last_verified_up": false,
|
|
94
96
|
"error_note": null,
|
|
95
97
|
"dns_note": null,
|
|
98
|
+
"dns_resolves": true,
|
|
96
99
|
"torn_down_at": null,
|
|
97
100
|
"open_intent": null
|
|
98
101
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.21.1",
|
|
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.
|
|
3
|
+
"version": "1.21.1",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8659,5 +8659,40 @@
|
|
|
8659
8659
|
"id": "1004513",
|
|
8660
8660
|
"text": "A goal now finishes by itself when its last task ships. Before, if someone ticked off all of a goal's done-when criteria while one task was still being worked on, the goal stayed open forever once that task landed — it took ti"
|
|
8661
8661
|
}
|
|
8662
|
+
],
|
|
8663
|
+
"1.21.0": [],
|
|
8664
|
+
"1.21.1": [
|
|
8665
|
+
{
|
|
8666
|
+
"id": "1004522",
|
|
8667
|
+
"text": "The Governor City design now lives in the project itself, not only in a private design canvas. This is the design where a governor's home is a small animated city and each part of the government is a building. All six design f"
|
|
8668
|
+
},
|
|
8669
|
+
{
|
|
8670
|
+
"id": "1004518",
|
|
8671
|
+
"text": "If GitHub refuses a Release for lack of permission, the deploy page now names the exact thing to fix: the server's access token or its GitHub App, whichever it uses."
|
|
8672
|
+
},
|
|
8673
|
+
{
|
|
8674
|
+
"id": "1004506",
|
|
8675
|
+
"text": "New projects now open their hall already in genesis. When a project is created through the startup flow, the platform marks it, and on the founder's first visit the hall switches into founding mode, once and safely. Projects m"
|
|
8676
|
+
},
|
|
8677
|
+
{
|
|
8678
|
+
"id": "1004521",
|
|
8679
|
+
"text": "Releases Bongos 1.21.0, the first bigger-step update, so we can watch cloudbongos.com take it on its own."
|
|
8680
|
+
},
|
|
8681
|
+
{
|
|
8682
|
+
"id": "1004515",
|
|
8683
|
+
"text": "The new way to start a project is now agreed. It is one short flow of six screens with no progress bar, plain words, and a calm night-time look. Before anything is created, you confirm every setting we filled in for you."
|
|
8684
|
+
},
|
|
8685
|
+
{
|
|
8686
|
+
"id": "1001004",
|
|
8687
|
+
"text": "Every session's complete conversation, with secrets removed, is now saved on the server so the review team can read what was actually asked and answered. Only the owner or an administrator can read it, and each administrator r"
|
|
8688
|
+
},
|
|
8689
|
+
{
|
|
8690
|
+
"id": "1004514",
|
|
8691
|
+
"text": "When a project is updated from the deploy page, the update now always saves its new version, even on projects whose history has no author name set. Before, those projects quietly kept the update unsaved, so the next update tri"
|
|
8692
|
+
},
|
|
8693
|
+
{
|
|
8694
|
+
"id": "1004505",
|
|
8695
|
+
"text": "A new project's address now works the first time you open it. The platform creates the address before anything else, and the hub only links it once the platform has seen it answer. Before, an early click could leave your home"
|
|
8696
|
+
}
|
|
8662
8697
|
]
|
|
8663
8698
|
}
|
package/scripts/gds/provision.js
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
// composes the docs/recipes/cloudbongos-standup.md runbook into ONE idempotent,
|
|
14
14
|
// dry-run-default, drift-reconciling flow (ADR 0016 / ADR 0031 §9.4 trust boundary):
|
|
15
15
|
//
|
|
16
|
-
//
|
|
16
|
+
// DNS A record FIRST (task 1004505 — the name must exist before anyone is shown it)
|
|
17
|
+
// → server (a process on an allocated port — co-tenant or cloud-host)
|
|
17
18
|
// → createdb + GDS-only migrate (MEDUSA_GDS_ONLY=1)
|
|
18
19
|
// → write config + per-instance /etc/<inst>/web.env
|
|
19
20
|
// → install + enable a per-instance systemd unit (+ allocate a port)
|
|
@@ -251,8 +252,8 @@ async function provisionInstance(inst, deps) {
|
|
|
251
252
|
|
|
252
253
|
if (isByoHost(inst.hosting_shape)) return recordByoHost(inst, deps); // stand up NOTHING, ABOVE every preflight — reasoning in provision-byo-host.js's header (ADR 0323)
|
|
253
254
|
// ── preflight: DNS token scope (task 1980) ─────────────────────────────────
|
|
254
|
-
// BEFORE committing to the address (
|
|
255
|
-
//
|
|
255
|
+
// BEFORE committing to the address (the step-0 DNS UPSERT /
|
|
256
|
+
// allocating a port), verify the control-plane Cloudflare token can actually reach
|
|
256
257
|
// the domain's zone. Turns the Emersonian standup's confusing mid-flow step-5 DNS
|
|
257
258
|
// failure into an up-front, actionable stop with no half-done provision. Read-only.
|
|
258
259
|
if (inst.domain && CONFIG.dnsHook) {
|
|
@@ -292,6 +293,64 @@ async function provisionInstance(inst, deps) {
|
|
|
292
293
|
const gap = caddyCheck(boxExec, { domain: inst.domain });
|
|
293
294
|
if (gap) return { ok: false, error: `Caddy preflight failed: ${gap}` };
|
|
294
295
|
}
|
|
296
|
+
// ── step 0: DNS FIRST — Cloudflare A-record UPSERT for the instance's domain (P4) ──
|
|
297
|
+
// The first mutation of a standup, ahead of the status flip and every step after it
|
|
298
|
+
// (task 1004505, BV2.PS17). The hub can show a project's address from the moment it
|
|
299
|
+
// is created, and a lookup made before the record exists is cached as "not found" by
|
|
300
|
+
// the visitor's resolver for the zone's negative TTL (30 min on cloudbongos.com) — a
|
|
301
|
+
// founder's first click used to lock their own home network out of the project for
|
|
302
|
+
// half an hour. Created here, the name answers before any surface can offer it; the
|
|
303
|
+
// hub also holds every link until a platform lookup has seen it answer (dns_resolves).
|
|
304
|
+
// The A-record target (recordIp):
|
|
305
|
+
// • co-tenant → PROVISION_PUBLIC_IP (the box it runs on — under remote-exec that
|
|
306
|
+
// is the co-hosting box, ADR 0130).
|
|
307
|
+
// • standalone → the CONTROL PLANE's own IP (task 2074): it runs control-plane-
|
|
308
|
+
// local, so its domain must point HERE, not at the co-hosting box.
|
|
309
|
+
// PROVISION_CONTROL_PLANE_IP when remote-exec splits the two boxes;
|
|
310
|
+
// else PROVISION_PUBLIC_IP (all-local: one and the same machine).
|
|
311
|
+
// Remote-exec set but no control-plane IP configured → null, which
|
|
312
|
+
// makes this step fail loud rather than point the domain at the wrong box.
|
|
313
|
+
const recordIp = standalone ? CONFIG.controlPlaneIp || (CONFIG.remoteHost ? null : CONFIG.publicIp) : CONFIG.publicIp;
|
|
314
|
+
// Formalized in P4 (#1945): the shared box-dns-cloudflare.sh hook UPSERTs an A
|
|
315
|
+
// record pointing the domain at recordIp, in the domain's own zone. It runs on the
|
|
316
|
+
// CONTROL PLANE for every shape: the hook needs the Cloudflare token, which never
|
|
317
|
+
// leaves the control plane. A DNS FAILURE SURFACES (the task-1180 lesson: never
|
|
318
|
+
// report provision success while the A record silently didn't land) — and, being
|
|
319
|
+
// first, it now fails before anything else was touched.
|
|
320
|
+
let dnsUpserted = false;
|
|
321
|
+
if (inst.domain && CONFIG.dnsHook) {
|
|
322
|
+
if (!recordIp) {
|
|
323
|
+
const ipEnvHint = standalone ? 'PROVISION_CONTROL_PLANE_IP (standalone)' : 'PROVISION_PUBLIC_IP (co-tenant)';
|
|
324
|
+
if (apply) throw new Error(`cannot UPSERT DNS for ${inst.domain}: no target IP — set ${ipEnvHint} in the control-plane box.env`);
|
|
325
|
+
log(` [dns] would upsert A ${inst.domain} once the target IP is known (set ${ipEnvHint})`);
|
|
326
|
+
} else {
|
|
327
|
+
// Standalone runs control-plane-local under the *.cloudbongos.com wildcard origin
|
|
328
|
+
// cert (task 2074/2077) → proxied=true (CF edge fronts it, IP hidden); co-tenant
|
|
329
|
+
// stays proxied=false so their on-demand ACME challenge (step 6) reaches the origin.
|
|
330
|
+
const dnsEnv = dnsUpsertEnv(inst, { publicIp: recordIp, proxied: standalone });
|
|
331
|
+
log(` [dns] upsert A ${inst.domain} -> ${recordIp} (zone ${dnsEnv.CLOUDFLARE_ZONE}, proxied=${dnsEnv.BOX_PROXIED === '1'}) via ${CONFIG.dnsHook}`);
|
|
332
|
+
// controlExec, NOT exec: the DNS hook needs the Cloudflare token, which lives
|
|
333
|
+
// only on the control plane — it must never run on the (token-free) co-hosting
|
|
334
|
+
// box even when the box-mutating steps below go there over SSH (task 2065).
|
|
335
|
+
controlExec(CONFIG.dnsHook, { env: dnsEnv });
|
|
336
|
+
dnsUpserted = true;
|
|
337
|
+
// The clear half of the birth-path note (F16): a revive after the operator
|
|
338
|
+
// configured the hook must retire the stale "point your A record" sentence.
|
|
339
|
+
if (apply && provisioning.recordDnsNote) {
|
|
340
|
+
await provisioning.recordDnsNote(db, inst.id, null).catch(() => {});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
log(' [dns] skipped (no domain, or BOX_DNS_HOOK unset)');
|
|
345
|
+
// Birth-path twin of the domain-attach note (F16): a domain with no hook is
|
|
346
|
+
// the same silent skip here, so the owner card gets the same sentence.
|
|
347
|
+
if (apply && inst.domain) {
|
|
348
|
+
if (provisioning.recordDnsNote) await provisioning.recordDnsNote(db, inst.id,
|
|
349
|
+
`Automatic DNS isn't set up on this platform — point ${inst.domain}'s A record at ${recordIp || 'your server'}, and this card will show when it answers.`
|
|
350
|
+
).catch(() => {});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
295
354
|
if (apply) await provisioning.setInstanceStatus(db, inst.id, 'provisioning');
|
|
296
355
|
const patch = {};
|
|
297
356
|
|
|
@@ -318,16 +377,6 @@ async function provisionInstance(inst, deps) {
|
|
|
318
377
|
}
|
|
319
378
|
|
|
320
379
|
// ── step 1: server ───────────────────────────────────────────────────────
|
|
321
|
-
// The A-record target for step 5's DNS UPSERT:
|
|
322
|
-
// • co-tenant → PROVISION_PUBLIC_IP (the box it runs on — under remote-exec that
|
|
323
|
-
// is the co-hosting box, ADR 0130).
|
|
324
|
-
// • standalone → the CONTROL PLANE's own IP (task 2074): it runs control-plane-
|
|
325
|
-
// local, so its domain must point HERE, not at the co-hosting box.
|
|
326
|
-
// PROVISION_CONTROL_PLANE_IP when remote-exec splits the two boxes;
|
|
327
|
-
// else PROVISION_PUBLIC_IP (all-local: one and the same machine).
|
|
328
|
-
// Remote-exec set but no control-plane IP configured → null, which
|
|
329
|
-
// makes step 5 fail loud rather than point the domain at the wrong box.
|
|
330
|
-
const recordIp = standalone ? CONFIG.controlPlaneIp || (CONFIG.remoteHost ? null : CONFIG.publicIp) : CONFIG.publicIp;
|
|
331
380
|
// Co-tenant OR standalone: a process bound to an allocated port on the box it runs
|
|
332
381
|
// on. Co-tenant shares that box's checkout ($0, cloudbongos.com model — under
|
|
333
382
|
// remote-exec, the co-hosting box); standalone (ADR 0108) runs from its OWN repo
|
|
@@ -433,46 +482,7 @@ async function provisionInstance(inst, deps) {
|
|
|
433
482
|
log(' [backup] create backup dir + install + enable nightly DB backup timer');
|
|
434
483
|
pm.installBackup(inst, { exec: boxExec, writeFile: boxWriteFile, sudoP, log });
|
|
435
484
|
|
|
436
|
-
//
|
|
437
|
-
// Formalized in P4 (#1945): the shared box-dns-cloudflare.sh hook UPSERTs an A
|
|
438
|
-
// record pointing the domain at its target IP (recordIp), in the domain's own
|
|
439
|
-
// zone, proxied=false so Caddy's on-demand ACME challenge (step 6) reaches the
|
|
440
|
-
// origin (dnsUpsertEnv). It runs on the CONTROL PLANE for every shape: the hook
|
|
441
|
-
// needs the Cloudflare token, which never leaves the control plane.
|
|
442
|
-
// Unlike P3's best-effort allowFail, a DNS FAILURE now SURFACES (the task-1180
|
|
443
|
-
// lesson: never report provision success while the A record silently didn't land).
|
|
444
|
-
if (inst.domain && CONFIG.dnsHook) {
|
|
445
|
-
if (!recordIp) {
|
|
446
|
-
const ipEnvHint = standalone ? 'PROVISION_CONTROL_PLANE_IP (standalone)' : 'PROVISION_PUBLIC_IP (co-tenant)';
|
|
447
|
-
if (apply) throw new Error(`cannot UPSERT DNS for ${inst.domain}: no target IP — set ${ipEnvHint} in the control-plane box.env`);
|
|
448
|
-
log(` [dns] would upsert A ${inst.domain} once the target IP is known (set ${ipEnvHint})`);
|
|
449
|
-
} else {
|
|
450
|
-
// Standalone runs control-plane-local under the *.cloudbongos.com wildcard origin
|
|
451
|
-
// cert (task 2074/2077) → proxied=true (CF edge fronts it, IP hidden); co-tenant
|
|
452
|
-
// stays proxied=false so their on-demand ACME challenge reaches the origin.
|
|
453
|
-
const dnsEnv = dnsUpsertEnv(inst, { publicIp: recordIp, proxied: standalone });
|
|
454
|
-
log(` [dns] upsert A ${inst.domain} -> ${recordIp} (zone ${dnsEnv.CLOUDFLARE_ZONE}, proxied=${dnsEnv.BOX_PROXIED === '1'}) via ${CONFIG.dnsHook}`);
|
|
455
|
-
// controlExec, NOT exec: the DNS hook needs the Cloudflare token, which lives
|
|
456
|
-
// only on the control plane — it must never run on the (token-free) co-hosting
|
|
457
|
-
// box even when the box-mutating steps above went there over SSH (task 2065).
|
|
458
|
-
controlExec(CONFIG.dnsHook, { env: dnsEnv });
|
|
459
|
-
// The clear half of the birth-path note (F16): a revive after the operator
|
|
460
|
-
// configured the hook must retire the stale "point your A record" sentence.
|
|
461
|
-
if (apply && provisioning.recordDnsNote) {
|
|
462
|
-
await provisioning.recordDnsNote(db, inst.id, null).catch(() => {});
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
} else {
|
|
466
|
-
log(' [dns] skipped (no domain, or BOX_DNS_HOOK unset)');
|
|
467
|
-
// Birth-path twin of the domain-attach note (F16): a domain with no hook is
|
|
468
|
-
// the same silent skip here, so the owner card gets the same sentence.
|
|
469
|
-
if (apply && inst.domain) {
|
|
470
|
-
if (provisioning.recordDnsNote) await provisioning.recordDnsNote(db, inst.id,
|
|
471
|
-
`Automatic DNS isn't set up on this platform — point ${inst.domain}'s A record at ${recordIp || 'your server'}, and this card will show when it answers.`
|
|
472
|
-
).catch(() => {});
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
485
|
+
// (step 5, the DNS UPSERT, moved to step 0 at the top — task 1004505.)
|
|
476
486
|
// A re-point on a PRE-ACTIVE instance records its release here and never
|
|
477
487
|
// reaches the attach leg (that one no-ops for anything but 'active'), so this
|
|
478
488
|
// run owns the drain — and an `error` row is frequently a partial standup that
|
|
@@ -542,6 +552,20 @@ async function provisionInstance(inst, deps) {
|
|
|
542
552
|
// standalone paths never cleared it). The runner hands its own observation
|
|
543
553
|
// to the liveness overlay so the card gets an honest verdict immediately
|
|
544
554
|
// instead of waiting ~5 min for the first sweep.
|
|
555
|
+
// ── step 7c: the platform's own lookup (task 1004505) ─────────────────────
|
|
556
|
+
// The record went in at step 0, minutes ago, so this lookup cannot race it into a
|
|
557
|
+
// cached miss. An answer lets the hub link the address the moment the row reads
|
|
558
|
+
// active, instead of waiting for the first liveness sweep. Only for a record this
|
|
559
|
+
// run created: a manual-DNS domain is the owner's to point, and the sweep finds it.
|
|
560
|
+
if (apply && dnsUpserted && provisioning.lookupAnswers && provisioning.recordDnsResolved) {
|
|
561
|
+
if (await provisioning.lookupAnswers(inst.domain, { lookup: deps.lookup })) {
|
|
562
|
+
await provisioning.recordDnsResolved(db, inst.id).catch(() => {});
|
|
563
|
+
log(` [dns] ${inst.domain} answers — the hub may link it now`);
|
|
564
|
+
} else {
|
|
565
|
+
log(` [dns] ${inst.domain} does not answer the control plane yet — the hub holds its link until the sweep sees it`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
545
569
|
if (apply) {
|
|
546
570
|
await provisioning.setInstanceStatus(db, inst.id, 'active', { ...patch, error_note: null, provisioned_at: new Date().toISOString() });
|
|
547
571
|
// The overlay column is DOMAIN-scoped when a domain exists (the sweep
|
|
@@ -34,6 +34,7 @@ const fs = require('fs');
|
|
|
34
34
|
const os = require('os');
|
|
35
35
|
const path = require('path');
|
|
36
36
|
const { cliClient, loadSession, arg } = require('./cli-lib');
|
|
37
|
+
const { uploadSessionTranscript } = require('./session-transcript-upload');
|
|
37
38
|
const { buildSessionDigest, collectEnvSecrets, foldSubagentUsage } = require('./ship');
|
|
38
39
|
|
|
39
40
|
|
|
@@ -115,8 +116,11 @@ function parseTranscript(transcriptPath) {
|
|
|
115
116
|
|
|
116
117
|
async function main() {
|
|
117
118
|
// Watchdog: a SessionEnd hook must never stall session exit. If the read or
|
|
118
|
-
// POST hangs (slow/offline network — fetch has no default timeout), bail.
|
|
119
|
-
|
|
119
|
+
// POST hangs (slow/offline network — fetch has no default timeout), bail. 25s,
|
|
120
|
+
// not the old 6s, because the full transcript (ADR 0051, up to ~12 MB) now
|
|
121
|
+
// uploads after the digest; the .claude/settings.json hook timeout (30s) is the
|
|
122
|
+
// outer bound, and a hang still just drops the upload, never blocks exit.
|
|
123
|
+
const watchdog = setTimeout(() => process.exit(0), 25000);
|
|
120
124
|
watchdog.unref();
|
|
121
125
|
try {
|
|
122
126
|
const stdinRaw = await readStdin();
|
|
@@ -143,7 +147,8 @@ async function main() {
|
|
|
143
147
|
const extraSecrets = collectEnvSecrets();
|
|
144
148
|
if (session.token) extraSecrets.push(session.token);
|
|
145
149
|
|
|
146
|
-
const
|
|
150
|
+
const entries = parseTranscript(transcriptPath);
|
|
151
|
+
const digest = buildSessionDigest(entries, { extraSecrets });
|
|
147
152
|
// task 1003435: fold this session's subagents subtree into model_usage, the
|
|
148
153
|
// same way the on-ship upload does — the whole session's tokens, not just the
|
|
149
154
|
// interactive thread's.
|
|
@@ -174,6 +179,9 @@ async function main() {
|
|
|
174
179
|
console.error(
|
|
175
180
|
`session-digest: stored full session (${digest.metrics.assistant_turns} turns, ${digest.totalTokens} tokens, ${digest.metrics.tool_error_count} tool errors).`
|
|
176
181
|
);
|
|
182
|
+
// ADR 0051: every session's FULL scrubbed transcript, after the digest (the
|
|
183
|
+
// server builds the transcript row from the digest's). Non-fatal.
|
|
184
|
+
console.error(`session-digest: ${await uploadSessionTranscript(api, sessionId, entries, extraSecrets)}`);
|
|
177
185
|
} else {
|
|
178
186
|
console.error(`session-digest: skipped (POST ${r && r.status}${r && r.data && r.data.error ? ' ' + r.data.error : ''}).`);
|
|
179
187
|
}
|