@bongos/core 1.20.60 → 1.20.62

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 (45) hide show
  1. package/.bongos-core.json +95 -40
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +6 -2
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/adr/0357-the-update-rule-set-on-deploy-is-the-one-the-sweep-follows.md +2 -0
  8. package/docs/api/openapi.json +112 -3
  9. package/docs/api-reference.md +5 -4
  10. package/docs/copy-inventory.md +377 -328
  11. package/docs/copy-registry.json +894 -421
  12. package/docs/module-api-changelog.md +4 -0
  13. package/docs/page-inventory.json +4 -1
  14. package/docs/page-readings.json +546 -483
  15. package/docs/recipes/upgrading-the-core.md +30 -1
  16. package/modules/autonomy/db.js +13 -6
  17. package/modules/autonomy/migrations/autonomy_004_runner_identity.sql +30 -0
  18. package/modules/autonomy/routes/autonomy.js +16 -0
  19. package/modules/autonomy/runner-health.js +66 -5
  20. package/modules/hall-ui/public/gate.js +24 -8
  21. package/modules/provisioning/demo.js +176 -0
  22. package/modules/provisioning/migrations/provisioning_035_demo.sql +26 -0
  23. package/modules/provisioning/module.json +5 -3
  24. package/modules/provisioning/pollers/demo-archive.js +61 -0
  25. package/modules/provisioning/provisioning.js +2 -2
  26. package/modules/provisioning/routes/demo.js +54 -0
  27. package/modules/provisioning/routes/provisioning.js +7 -4
  28. package/modules/public-landing/public/projects-demo.states.json +53 -0
  29. package/modules/public-landing/public/projects.html +300 -23
  30. package/modules/public-landing/public/projects.probes.json +3 -3
  31. package/modules/public-landing/public/projects.states.json +2 -1
  32. package/modules/ui-design/kit/fixtures/provisioning-instances-demo.json +114 -0
  33. package/package-lock.json +2 -2
  34. package/package.json +1 -1
  35. package/release-notes.json +20 -0
  36. package/scripts/gds/autobongos-run.js +43 -2
  37. package/scripts/gds/update-channel.js +11 -2
  38. package/scripts/gds/update-sweep.js +717 -0
  39. package/src/module-api.js +1 -1
  40. package/tests/autonomy_runner_identity.mjs +215 -0
  41. package/tests/update_channel_db.mjs +13 -6
  42. package/tests/update_subscription_engine.mjs +5 -5
  43. package/tests/update_sweep_home.mjs +100 -0
  44. package/tests/wizard_demo.mjs +264 -0
  45. package/tests/wizard_front_door.mjs +16 -20
@@ -39,7 +39,8 @@
39
39
  "in-new": { "auth": true, "url": "/projects?view=new", "expect": { "visible": ["#panel-terms", "#termsAck"], "hidden": ["#qWrap", "#view-new .steps", "#panel-name"] } },
40
40
  "in-new-terms-refused": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#panel-terms button[type=submit]"],["wait",300]], "expect": {"visible":["#panel-terms","#h-terms"],"hidden":["#panel-fork"],"text":[["#h-terms","Tick the box to go on. It’s how your project records that you acknowledged the terms."]]} },
41
41
  "in-new-fork": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-fork",".doorPill","#wizDemo",".forkStage",".forkRoutes"],"hidden":["#panel-terms","#view-new .steps","#demoSoon",".bongoCell"]} },
42
- "in-new-fork-demo": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#wizDemo"],["wait",200]], "expect": {"visible":["#panel-fork","#demoSoon"],"attr":[["#wizDemo","aria-expanded","true"]]} },
42
+ "in-new-fork-demo": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#wizDemo"],["wait",300]], "expect": {"visible":["#panel-fork","#demoSetup","#demoFitVerdict","#demoRing"],"hidden":["#forkIntro","#demoReady"],"attr":[["#wizDemo","aria-expanded","true"]]} },
43
+ "in-new-demo-ready": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#wizDemo"],["wait",300],["click","[data-demo-step=\"hours\"][data-by=\"1\"]"],["click","#demoNext1"],["wait",300]], "expect": {"visible":["#demoReady",".demoLedger","#demoInvite1","#demoInvite2"],"hidden":["#demoSetup","#forkIntro"],"text":[["#demoLHours","5 hours each"]]} },
43
44
  "in-new-name": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-name","#newName","#view-new .steps"],"hidden":["#panel-fork","#panel-terms"]} },
44
45
  "out-new": { "auth": false, "url": "/projects?view=new", "expect": { "hidden": ["#qWrap"] } },
45
46
  "in-new-type": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-type"],"hidden":["#panel-name","#qWrap"]} },
@@ -0,0 +1,114 @@
1
+ {
2
+ "_": "The demo route (task 1004420): provisioning-instances.json with project 101 marked as a demo whose time box has run out, so its page asks keep-your-planet. Only projects-demo.states.json points at this.",
3
+ "instances": [
4
+ {
5
+ "id": 101,
6
+ "slug": "demo",
7
+ "status": "active",
8
+ "domain": "demo.cloudbongos.com",
9
+ "target_ref": "builder/demo",
10
+ "hosting_shape": "co-tenant",
11
+ "onboard_mode": "scaffold",
12
+ "created_at": "2026-08-01T10:00:00.000Z",
13
+ "provisioned_at": "2026-08-01T10:20:00.000Z",
14
+ "updated_at": "$now-3m",
15
+ "last_verified_at": "$now-3m",
16
+ "last_verified_up": true,
17
+ "error_note": null,
18
+ "dns_note": null,
19
+ "torn_down_at": null,
20
+ "open_intent": null,
21
+ "demo": {
22
+ "people": 3,
23
+ "hours_each": 4,
24
+ "builder_hours": 12,
25
+ "started_at": "2026-09-28T09:00:00Z",
26
+ "ends_at": "2026-09-28T13:00:00Z",
27
+ "remaining_seconds": 0,
28
+ "time_up": true,
29
+ "outcome": null,
30
+ "offline_on": "2026-10-28T09:00:00.000Z",
31
+ "offline_soon": false
32
+ }
33
+ },
34
+ {
35
+ "id": 102,
36
+ "slug": "harbor",
37
+ "status": "provisioning",
38
+ "planet_template": "blob-frost-tint",
39
+ "planet_accent": "sun-lit",
40
+ "domain": null,
41
+ "target_ref": "builder/harbor",
42
+ "hosting_shape": "cloud-host",
43
+ "owner_monthly_usd": null,
44
+ "onboard_mode": "scaffold",
45
+ "created_at": "$now-6m",
46
+ "provisioned_at": null,
47
+ "updated_at": "$now-2m",
48
+ "last_verified_at": null,
49
+ "last_verified_up": null,
50
+ "error_note": null,
51
+ "dns_note": null,
52
+ "torn_down_at": null,
53
+ "open_intent": {
54
+ "action": "standup",
55
+ "state": "running",
56
+ "created_at": "$now-6m"
57
+ }
58
+ },
59
+ {
60
+ "id": 103,
61
+ "slug": "tidepool",
62
+ "status": "error",
63
+ "domain": "tidepool.cloudbongos.com",
64
+ "target_ref": "builder/tidepool",
65
+ "hosting_shape": "co-tenant",
66
+ "onboard_mode": "adopt",
67
+ "created_at": "2026-08-20T09:00:00.000Z",
68
+ "provisioned_at": null,
69
+ "updated_at": "$now-1d",
70
+ "last_verified_at": null,
71
+ "last_verified_up": null,
72
+ "error_note": "The deploy could not read the repository: it is private, and the deploy key only reads public ones.",
73
+ "dns_note": null,
74
+ "torn_down_at": null,
75
+ "open_intent": null
76
+ },
77
+ {
78
+ "id": 104,
79
+ "slug": "old-lantern",
80
+ "status": "torn_down",
81
+ "domain": "old-lantern.cloudbongos.com",
82
+ "target_ref": "builder/old-lantern",
83
+ "hosting_shape": "co-tenant",
84
+ "onboard_mode": "scaffold",
85
+ "created_at": "2026-07-02T09:00:00.000Z",
86
+ "provisioned_at": "2026-07-02T09:30:00.000Z",
87
+ "updated_at": "2026-08-15T12:00:00.000Z",
88
+ "last_verified_at": "2026-08-15T11:50:00.000Z",
89
+ "last_verified_up": true,
90
+ "error_note": null,
91
+ "dns_note": null,
92
+ "torn_down_at": "2026-08-15T12:00:00.000Z",
93
+ "open_intent": null
94
+ },
95
+ {
96
+ "id": 105,
97
+ "slug": "mooring",
98
+ "status": "active",
99
+ "domain": "mooring.cloudbongos.com",
100
+ "target_ref": "builder/mooring",
101
+ "hosting_shape": "co-tenant",
102
+ "onboard_mode": "scaffold",
103
+ "created_at": "2026-08-10T09:00:00.000Z",
104
+ "provisioned_at": "2026-08-10T09:20:00.000Z",
105
+ "updated_at": "$now-20m",
106
+ "last_verified_at": "$now-20m",
107
+ "last_verified_up": false,
108
+ "error_note": null,
109
+ "dns_note": null,
110
+ "torn_down_at": null,
111
+ "open_intent": null
112
+ }
113
+ ]
114
+ }
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.20.60",
3
+ "version": "1.20.62",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.20.60",
9
+ "version": "1.20.62",
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.20.60",
3
+ "version": "1.20.62",
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",
@@ -8477,5 +8477,25 @@
8477
8477
  "id": "1004421",
8478
8478
  "text": "Every project can now have its own look, so builders who work in several projects always know which hall they are in. Creating a project has a new Look step: four ready-made looks or your own colour, a switch to tint dark mode"
8479
8479
  }
8480
+ ],
8481
+ "1.20.61": [
8482
+ {
8483
+ "id": "1004407",
8484
+ "text": "The Gate page now shows, for each Autobongos runner, which PC it is on, which Claude account it is spending, which code version it runs (and whether that is behind), and says \"Restarting\" while it relaunches for new code inste"
8485
+ }
8486
+ ],
8487
+ "1.20.62": [
8488
+ {
8489
+ "id": "1004047",
8490
+ "text": "Every live project now runs a version whose update banner works, so each owner sees when an update is waiting."
8491
+ },
8492
+ {
8493
+ "id": "1004420",
8494
+ "text": "\"Try the demo\" now works. A small team picks how many people and how many hours each, writes an idea, and gets a first read of what fits. The demo picks the settings for them (the founder decides, no rewards, all managed, priv"
8495
+ },
8496
+ {
8497
+ "id": "1004307",
8498
+ "text": "The automatic update on cloudbongos.com will now always run the newest version of itself, instead of a copy stuck from August."
8499
+ }
8480
8500
  ]
8481
8501
  }
@@ -368,7 +368,7 @@ const MAX_CLAIM_ATTEMPTS = 5;
368
368
  const LIMIT_UNKNOWN_RESET_MS = 30 * 60 * 1000;
369
369
 
370
370
  function newRunnerState() {
371
- return { setAside: new Map(), queueGate: null, limitUntil: null, diskHead: null };
371
+ return { setAside: new Map(), queueGate: null, limitUntil: null, diskHead: null, mainHead: null };
372
372
  }
373
373
  const RUNNER_STATE = newRunnerState();
374
374
 
@@ -703,6 +703,43 @@ function writeHeartbeat(now = Date.now(), runnerState = RUNNER_STATE) {
703
703
  // is the signal.
704
704
  const RUNNER_STARTED_AT = new Date().toISOString();
705
705
 
706
+ // claudeAccountLabel — WHICH Claude account this machine's workers spend
707
+ // (task 1004407), as a label and never a secret. Workers inherit
708
+ // ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN from this process
709
+ // (WORKER_ENV_ALLOW), and either one outranks the stored login, so those are
710
+ // named first, by the variable that is set and never by its value. Otherwise it
711
+ // is the login Claude Code itself records in ~/.claude.json (oauthAccount): the
712
+ // email, plus the organisation when it has one. Read on every beat, so signing
713
+ // a different account in shows up without a restart; null when nothing can be
714
+ // read, which the card leaves blank rather than guessing.
715
+ function claudeAccountLabel({ env = process.env, readFile = (p) => fs.readFileSync(p, 'utf8') } = {}) {
716
+ if (env.ANTHROPIC_API_KEY) return 'an API key (ANTHROPIC_API_KEY)';
717
+ if (env.CLAUDE_CODE_OAUTH_TOKEN) return 'a Claude token (CLAUDE_CODE_OAUTH_TOKEN)';
718
+ try {
719
+ const dir = env.CLAUDE_CONFIG_DIR || os.homedir();
720
+ const acct = JSON.parse(readFile(path.join(dir, '.claude.json'))).oauthAccount || {};
721
+ const email = typeof acct.emailAddress === 'string' ? acct.emailAddress.trim() : '';
722
+ if (!email) return null;
723
+ const org = typeof acct.organizationName === 'string' ? acct.organizationName.trim() : '';
724
+ return (org ? `${email} · ${org}` : email).slice(0, 200);
725
+ } catch (_) { return null; }
726
+ }
727
+
728
+ // heartbeatIdentity — the fields that say which code and which account (task
729
+ // 1004407). Undefined fields are dropped, so a runner that cannot read its own
730
+ // commit sends nothing for it rather than an empty string the route would refuse.
731
+ function heartbeatIdentity(deps = {}) {
732
+ const st = deps.state || RUNNER_STATE;
733
+ const out = {
734
+ head: LOADED_HEAD || undefined,
735
+ disk_head: st.diskHead && st.diskHead !== LOADED_HEAD ? st.diskHead : undefined,
736
+ main_head: st.mainHead || undefined,
737
+ claude_account: (deps.claudeAccountLabel || claudeAccountLabel)() || undefined,
738
+ };
739
+ for (const k of Object.keys(out)) if (out[k] === undefined) delete out[k];
740
+ return out;
741
+ }
742
+
706
743
  async function postHeartbeat(fields = {}, deps = {}) {
707
744
  try {
708
745
  const api = deps.api || await (require('./cli-lib').cliClient)();
@@ -711,6 +748,7 @@ async function postHeartbeat(fields = {}, deps = {}) {
711
748
  host: os.hostname(),
712
749
  pid: process.pid,
713
750
  started_at: RUNNER_STARTED_AT,
751
+ ...heartbeatIdentity(deps),
714
752
  ...fields,
715
753
  },
716
754
  });
@@ -870,6 +908,9 @@ async function codeDrifted(deps = {}) {
870
908
  let tip = null;
871
909
  try { tip = await runFetchHead(deps); } catch (_) { return null; }
872
910
  if (!tip) return null;
911
+ // Kept for the heartbeat (task 1004407), so the hall can say "behind main"
912
+ // without the server ever reaching GitHub itself.
913
+ (deps.state || RUNNER_STATE).mainHead = tip;
873
914
  return tip !== base ? { from: base.slice(0, 8), to: String(tip).slice(0, 8) } : false;
874
915
  }
875
916
 
@@ -1147,6 +1188,6 @@ module.exports = {
1147
1188
  readArgv, pickTask, iteration, record, logPath, spawnWorker, workerEnv, WORKER_ENV_ALLOW, resolveWorkerBin,
1148
1189
  worktreeName, readFence, reconcileLeftoverClaims, pollSignals, waitOrJump, forever, CLAIM_PREFIX, codeDrifted, loadedHead, UPGRADE_EXIT_CODE, MIN_UPTIME_BEFORE_UPGRADE_MS,
1149
1190
  heartbeatPath, writeHeartbeat, readHeartbeat, pidAlive, anotherRunnerIsAlive, HEARTBEAT_STALE_MS,
1150
- isRunnerTree, RUNNER_TREE_RE, postHeartbeat, RUNNER_STARTED_AT, failureReason,
1191
+ isRunnerTree, RUNNER_TREE_RE, postHeartbeat, claudeAccountLabel, RUNNER_STARTED_AT, failureReason,
1151
1192
  newRunnerState, loadedFiles, supervisorTouched, refreshCheckout, LIMIT_UNKNOWN_RESET_MS, QUEUE_GATED_EXIT, CLAIM_SET_ASIDE_MS, MAX_CLAIM_ATTEMPTS, owedTaskIds,
1152
1193
  };
@@ -243,6 +243,14 @@ function loadSubscriptions({ instanceDir = process.cwd(), fsImpl = fs, file = SU
243
243
  //
244
244
  // A failure's reason is a fixed class, never psql's own text: stderr can name a DB user or host,
245
245
  // and the reason is printed into the sweep's log.
246
+ //
247
+ // WHICH DATABASE (task 1004307). UPDATE_CHANNEL_DB names it first. That is the one a control-plane
248
+ // unit should set, NOT PGDATABASE: the sweep hands its own env to every upgrade it spawns
249
+ // ({ ...process.env, ...inst.env }), so a unit-level PGDATABASE would reach any tenant whose roster
250
+ // entry names no database of its own, and that tenant's migrate would run against the control
251
+ // plane's database. UPDATE_CHANNEL_DB means nothing to the upgrade, so it cannot leak that way.
252
+ // It takes a bare db name or a connection string, as psql -d does.
253
+ const CHANNEL_DB_ENV = 'UPDATE_CHANNEL_DB';
246
254
  const CHANNEL_SOURCE_DEPLOY = 'deploy';
247
255
  const CHANNEL_SOURCE_ROSTER = 'roster';
248
256
  const DB_CHANNELS_SQL = 'SELECT slug, update_channel FROM provisioning_instances';
@@ -268,8 +276,8 @@ function channelLine(inst) {
268
276
  // { ok:false, reason } when there is no database to ask, psql is missing, or the table is absent
269
277
  // (an instance with the provisioning module off). The caller falls back to the roster and says why.
270
278
  function readDbChannels({ env = process.env, run = spawnSync } = {}) {
271
- const target = env.DATABASE_URL || env.PGDATABASE;
272
- if (!target) return { ok: false, reason: 'no DATABASE_URL or PGDATABASE set for the sweep' };
279
+ const target = env[CHANNEL_DB_ENV] || env.DATABASE_URL || env.PGDATABASE;
280
+ if (!target) return { ok: false, reason: `no ${CHANNEL_DB_ENV} (or DATABASE_URL / PGDATABASE) set for the sweep` };
273
281
  const r = run('psql', ['-d', target, '-At', '-F', '\t', '-v', 'ON_ERROR_STOP=1', '--no-psqlrc', '-c', DB_CHANNELS_SQL],
274
282
  { encoding: 'utf8', timeout: DB_CHANNELS_TIMEOUT_MS });
275
283
  if (!r || r.error) {
@@ -399,6 +407,7 @@ module.exports = {
399
407
  parseNpmViewVersions,
400
408
  loadSubscriptions,
401
409
  readDbChannels,
410
+ CHANNEL_DB_ENV,
402
411
  applyDbChannels,
403
412
  channelLine,
404
413
  psqlFailureClass,