@bongos/core 1.19.126 → 1.19.128

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.126",
6
- "core_contract": "1.19.126",
7
- "source_commit": "b56cedc2da8ef6691bd7990c6e3219896694e540",
5
+ "core_version": "1.19.128",
6
+ "core_contract": "1.19.128",
7
+ "source_commit": "15671cf5128a6b644791170f9b76c0637bcc0c2a",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-15T00:16:09.571Z",
9
+ "built_at": "2026-08-15T01:56:43.213Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 303,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1245,
14
+ "functional_verbatim": 1246,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1569,
20
- "tree_sha256": "5703791569f4c26556ad21f0bbcf605faf1b42cfd3631a1ce5f25a05269fd4b4",
19
+ "file_count": 1570,
20
+ "tree_sha256": "4df41f4612e88057c6058199bd929bb309af81367a8fd7435309497c2456ac1a",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -2367,7 +2367,7 @@
2367
2367
  {
2368
2368
  "path": "modules/builder-settings/builder-needs.js",
2369
2369
  "mode": "0000644",
2370
- "sha256": "0b40afc38f91ad053d5ecca380eb35d314a3f104fbe8096d51efeb3de131fc98"
2370
+ "sha256": "3cb7478d9e347147e331f0954a9178a1770918067a678efdb8caef96241dfb5a"
2371
2371
  },
2372
2372
  {
2373
2373
  "path": "modules/builder-settings/db.js",
@@ -3844,6 +3844,11 @@
3844
3844
  "mode": "0000644",
3845
3845
  "sha256": "ddbc1f7e659e716fcbb3ba3ec6c1237756f46dbd895bcb27a1dc925a7c782e76"
3846
3846
  },
3847
+ {
3848
+ "path": "modules/provisioning/migrations/provisioning_008_dns_note.sql",
3849
+ "mode": "0000644",
3850
+ "sha256": "8dc04a18ad77563a2bd73029bf13cab03fb5dadd1b62850fddbde475257e6a76"
3851
+ },
3847
3852
  {
3848
3853
  "path": "modules/provisioning/module.json",
3849
3854
  "mode": "0000644",
@@ -3862,7 +3867,7 @@
3862
3867
  {
3863
3868
  "path": "modules/provisioning/provisioning.js",
3864
3869
  "mode": "0000644",
3865
- "sha256": "59fd212df421b2bdcdfaef4871aa1927c371d3f7d6affafe44e810135acd12b6"
3870
+ "sha256": "90aa880898f6b3235e8b2a1a553695c63c29920968edb9c9af58364902118e33"
3866
3871
  },
3867
3872
  {
3868
3873
  "path": "modules/provisioning/rate-limit.js",
@@ -3872,7 +3877,7 @@
3872
3877
  {
3873
3878
  "path": "modules/provisioning/routes/provisioning.js",
3874
3879
  "mode": "0000644",
3875
- "sha256": "321db8769eda7c5f6f385e90edaeec70fc071f0bc2e53e6147705153b0d32d3a"
3880
+ "sha256": "eddf451d5ee5085563af258b5c605c445776c9e226587341f9b4b8b405492240"
3876
3881
  },
3877
3882
  {
3878
3883
  "path": "modules/provisioning/tests/liveness.mjs",
@@ -3882,7 +3887,7 @@
3882
3887
  {
3883
3888
  "path": "modules/provisioning/tests/provisioning.mjs",
3884
3889
  "mode": "0000644",
3885
- "sha256": "4a5c817d88771a62ccb5d26aea865976b7246a253b2ad7b00e11e3be856586ef"
3890
+ "sha256": "0dc18db288580ca00792194ce5f50a13aae8c445c14d7fc42a2095ab2379c93f"
3886
3891
  },
3887
3892
  {
3888
3893
  "path": "modules/provisioning/ui/.gitkeep",
@@ -4167,12 +4172,12 @@
4167
4172
  {
4168
4173
  "path": "package-lock.json",
4169
4174
  "mode": "0000644",
4170
- "sha256": "64edc372bb49cd5c30eb5a1d46f54361293de787d1717d2e116f52568ff93f2c"
4175
+ "sha256": "573c516fc294514ce5ed281eb161de21132ddbfd775c5eb63ae1636bd89fc854"
4171
4176
  },
4172
4177
  {
4173
4178
  "path": "package.json",
4174
4179
  "mode": "0000644",
4175
- "sha256": "c652ffaf3901e4ccc8f64ff9340f7f4f6c418ab738fc52f2b71450083d2a4e97"
4180
+ "sha256": "bfe73c49720864c119018c0ce9a8889271e7f67c4708ca4dbeaf5af26d277b11"
4176
4181
  },
4177
4182
  {
4178
4183
  "path": "public-docs/index.html",
@@ -4427,7 +4432,7 @@
4427
4432
  {
4428
4433
  "path": "public-landing/projects.html",
4429
4434
  "mode": "0000644",
4430
- "sha256": "6dcb73054fa58d3b0254f1d82a7f639ed465db2c2fd1e83e26f29b80532047e5"
4435
+ "sha256": "ac866c928701c41ac7f055f8807590a3db71eb2538502a54a6fca9ac1d79868d"
4431
4436
  },
4432
4437
  {
4433
4438
  "path": "public-landing/world/assets/clouds/cloud-island-large.png",
@@ -5082,7 +5087,7 @@
5082
5087
  {
5083
5088
  "path": "scripts/gds/provision.js",
5084
5089
  "mode": "0000644",
5085
- "sha256": "a10501b65207bbd72f9fc039d4cf1ef0be7f85dc832e8e95e1b26aa966e507b8"
5090
+ "sha256": "1925ed112ec553a95388b81cccc1fb003ba2a0e91a5a80c6e315b45895688d0f"
5086
5091
  },
5087
5092
  {
5088
5093
  "path": "scripts/gds/publish-credential-check.js",
@@ -5527,12 +5532,12 @@
5527
5532
  {
5528
5533
  "path": "src/bongos/db-kernel.js",
5529
5534
  "mode": "0000644",
5530
- "sha256": "fa80ba2c0bb742b83d637280e65965f450bc0387063e65dd06f6bd9171283f23"
5535
+ "sha256": "f31dd847ed641bf49013c345b1f08f9e2794a875dad777de10daeda2a4ae1cf3"
5531
5536
  },
5532
5537
  {
5533
5538
  "path": "src/bongos/db.js",
5534
5539
  "mode": "0000644",
5535
- "sha256": "64b0efa7c0a251266a8dbc00d43373fb1516fa5c88ba737220c969ec97a2e98b"
5540
+ "sha256": "d3dd606664855a94788751d13ab7fc62ccdc4a8df48f331ec97a5a81a8a6fa96"
5536
5541
  },
5537
5542
  {
5538
5543
  "path": "src/bongos/github-repos.js",
@@ -5687,7 +5692,7 @@
5687
5692
  {
5688
5693
  "path": "src/bongos/routes/me.js",
5689
5694
  "mode": "0000644",
5690
- "sha256": "e6c9e19a4b644f35da665cc48df21085d9a187e3b694a73af01f3d1b83c620b8"
5695
+ "sha256": "83657d8a2f8e1d81a80324350ddd983baf5346da3f88ce1f41c2211275f34440"
5691
5696
  },
5692
5697
  {
5693
5698
  "path": "src/bongos/routes/modules.js",
@@ -5757,7 +5762,7 @@
5757
5762
  {
5758
5763
  "path": "src/module-api.js",
5759
5764
  "mode": "0000644",
5760
- "sha256": "c64972eb393e195b4f24de3459036423077430a5cf643fd2e3a8da525ab3af56"
5765
+ "sha256": "e0b338840f48746ba4ad9bccd7dd4b80c36b844c2e16ea12bdf42f22ceb2eef5"
5761
5766
  },
5762
5767
  {
5763
5768
  "path": "src/module-loader/catalog.js",
@@ -6147,7 +6152,7 @@
6147
6152
  {
6148
6153
  "path": "tests/builder_needs.mjs",
6149
6154
  "mode": "0000644",
6150
- "sha256": "e3d8c5ac45d59f5d566547e2c406cf44ffa60f1b243afcb222b09eee68f5ee0d"
6155
+ "sha256": "49a8146cbb0ec5def5b48130ed61fdec6e9855f22fe5ad3a09b39ae2cb218ca8"
6151
6156
  },
6152
6157
  {
6153
6158
  "path": "tests/bump_version.mjs",
@@ -7297,7 +7302,7 @@
7297
7302
  {
7298
7303
  "path": "tests/provision.mjs",
7299
7304
  "mode": "0000644",
7300
- "sha256": "edb6c5da9ec32536d50f7cb648b81b026b7926f73f891feb0906fc7343b5b11d"
7305
+ "sha256": "5297caf5005fd26c8435aa374511882a6c2d232e43f07cdc6ec19d1168ee1cc1"
7301
7306
  },
7302
7307
  {
7303
7308
  "path": "tests/provisioning_default_domain.mjs",
@@ -7307,7 +7312,7 @@
7307
7312
  {
7308
7313
  "path": "tests/provisioning_domain_attach.mjs",
7309
7314
  "mode": "0000644",
7310
- "sha256": "fbbd7397fd03181e9c2ea58ed8accb1a9d9d66d4c904a09265161e1034b02f9f"
7315
+ "sha256": "856f434c8e1a8e8ad549c81689f1bbe27ff555e4ba08f9b485c3e9d46838a4a5"
7311
7316
  },
7312
7317
  {
7313
7318
  "path": "tests/provisioning_github_app_routes.mjs",
@@ -7332,7 +7337,7 @@
7332
7337
  {
7333
7338
  "path": "tests/public_landing_projects.mjs",
7334
7339
  "mode": "0000644",
7335
- "sha256": "2d4dea6cd6be287c1d7629009adc64fba526b1ebbe53a800ae43499e1b8e3f18"
7340
+ "sha256": "7f2fe0290fc0a429b54ca53d449453e9009a57e40bb8003b2718beef33be2309"
7336
7341
  },
7337
7342
  {
7338
7343
  "path": "tests/public_mirror_export.mjs",
@@ -90,13 +90,68 @@ function artKeyNeed(ctx) {
90
90
  // pure (ctx) => need|null function here — every renderer picks it up for free.
91
91
  const NEEDS = [artKeyNeed];
92
92
 
93
+ // ---- founding grace (task 1002739 / direction §5.6, idea 1000701) -----------
94
+ //
95
+ // A freshly-provisioned instance used to greet its founding owner with "your
96
+ // free art coverage has ended" — a demand as the payoff moment of the whole
97
+ // create flow. While the founding owner's first days are running, the roster
98
+ // leads with a welcome and every action_needed item is held at 'covered'
99
+ // (listed, honest, but not demanding). First-run is a time window, not a
100
+ // visit-counter: counting the first hall visit would end the grace on the very
101
+ // request that renders it.
102
+ //
103
+ // ctx.isFoundingOwner — is this builder the instance's first admitted builder?
104
+ // ctx.founderCreatedAt — that first builder's created_at (≈ the instance's
105
+ // birth on a newborn); the window anchors here.
106
+ // ctx.projectName — branding identity.productName, for the greeting.
107
+ // ctx.nowMs — injectable clock for tests; defaults to Date.now().
108
+ const FOUNDING_GRACE_DAYS = 7;
109
+ const FOUNDING_GRACE_MS = FOUNDING_GRACE_DAYS * 24 * 60 * 60 * 1000;
110
+
111
+ function foundingGraceActive(ctx = {}) {
112
+ if (!ctx.isFoundingOwner || !ctx.founderCreatedAt) return false;
113
+ const born = new Date(ctx.founderCreatedAt).getTime();
114
+ if (!Number.isFinite(born)) return false;
115
+ const now = typeof ctx.nowMs === 'number' ? ctx.nowMs : Date.now();
116
+ return now - born < FOUNDING_GRACE_MS;
117
+ }
118
+
119
+ function foundingWelcomeNeed(ctx) {
120
+ const name = ctx.projectName || 'your project';
121
+ return {
122
+ id: 'founding_welcome',
123
+ title: `Welcome to ${name}`,
124
+ what: 'Your project is standing, and this hall is where it gets built.',
125
+ learn_more_url: null,
126
+ state: 'covered',
127
+ severity: 'info',
128
+ message: `You made it — ${name} is up, and this hall is its workshop. ` +
129
+ 'Look around at your own pace; if the platform ever needs something from you, ' +
130
+ 'it will say so here once your first days are behind you.',
131
+ action: null,
132
+ };
133
+ }
134
+
93
135
  // Compute the live list for a builder. `ctx` carries everything the need
94
136
  // functions read (see each need's doc). Returns:
95
137
  // { items: Need[], action_needed_count, has_action_needed }
96
138
  // items is ordered: action_needed first (loudest), then covered, then satisfied,
97
- // so any renderer that takes "the first one" gets the most urgent.
139
+ // so any renderer that takes "the first one" gets the most urgent. During the
140
+ // founding grace nothing is action_needed, so the welcome (prepended before the
141
+ // stable sort) is what that renderer shows.
98
142
  function computeNeeds(ctx = {}) {
99
- const items = NEEDS.map((fn) => fn(ctx)).filter(Boolean);
143
+ const grace = foundingGraceActive(ctx);
144
+ let items = NEEDS.map((fn) => fn(ctx)).filter(Boolean);
145
+ if (grace) {
146
+ items = items.map((n) => n.state !== 'action_needed' ? n : {
147
+ ...n,
148
+ state: 'covered',
149
+ severity: 'info',
150
+ message: 'Nothing needed from you today — we’ll bring this up once your first days are behind you.',
151
+ action: null,
152
+ });
153
+ items.unshift(foundingWelcomeNeed(ctx));
154
+ }
100
155
  const order = { action_needed: 0, covered: 1, satisfied: 2 };
101
156
  items.sort((a, b) => (order[a.state] ?? 9) - (order[b.state] ?? 9));
102
157
  const actionNeeded = items.filter((n) => n.state === 'action_needed');
@@ -111,5 +166,7 @@ module.exports = {
111
166
  computeNeeds,
112
167
  // exported for tests / direct reuse
113
168
  artKeyNeed,
169
+ foundingGraceActive,
170
+ FOUNDING_GRACE_DAYS,
114
171
  SETTINGS_PATH,
115
172
  };
@@ -0,0 +1,15 @@
1
+ -- provisioning_008_dns_note.sql — transport the runner's "DNS was skipped" fact
2
+ -- to the owner's card (task 1002720 / QA-walk F16).
3
+ --
4
+ -- WHY. With BOX_DNS_HOOK unset, domain-attach wires env + Caddy but SKIPS the
5
+ -- A-record upsert — a fact that lived only in the runner's log while the owner's
6
+ -- card promised the address "will answer shortly" (it never would). This column
7
+ -- carries the outstanding manual step as a plain sentence the card renders.
8
+ --
9
+ -- OVERLAY, NOT A STATE (the provisioning_006 rule): nullable, no default,
10
+ -- never written by the lifecycle. NULL = nothing outstanding. A later attach
11
+ -- with a working hook clears it.
12
+ ALTER TABLE provisioning_instances ADD COLUMN IF NOT EXISTS dns_note text;
13
+
14
+ INSERT INTO schema_migrations (version) VALUES ('provisioning_008_dns_note')
15
+ ON CONFLICT (version) DO NOTHING;
@@ -198,6 +198,9 @@ function publicInstance(row) {
198
198
  // them differently ("live?" vs "unreachable"). Never collapse the two.
199
199
  last_verified_at: row.last_verified_at || null,
200
200
  last_verified_up: row.last_verified_up == null ? null : !!row.last_verified_up,
201
+ // The outstanding manual-DNS step, if the runner had no hook to point the
202
+ // A record (task 1002720 / F16). null = nothing outstanding.
203
+ dns_note: row.dns_note || null,
201
204
  cost_estimate_usd: Number(row.cost_estimate_usd || 0),
202
205
  error_note: row.error_note || null,
203
206
  created_at: row.created_at,
@@ -372,6 +375,30 @@ async function recordLivenessProbe(db, id, up) {
372
375
  return rows[0] || null;
373
376
  }
374
377
 
378
+ // Record (or clear, with null) the outstanding manual-DNS sentence the card
379
+ // renders (task 1002720 / F16). Same narrowness rule as recordLivenessProbe:
380
+ // no `status`, no `updated_at` — the runner's lifecycle authorship and the
381
+ // owner's card ordering stay untouched.
382
+ async function recordDnsNote(db, id, note) {
383
+ const { rows } = await db.query(
384
+ `UPDATE provisioning_instances SET dns_note = $2 WHERE id = $1 RETURNING id, dns_note`,
385
+ [id, note || null]
386
+ );
387
+ return rows[0] || null;
388
+ }
389
+
390
+ // Clear the error_note latch (task 1002720 / F19). The retire leg of the intent
391
+ // runner sets it when work stops; a later SUCCESSFUL run must release it, or the
392
+ // card shows "we stopped trying" forever on a healed instance. Same narrowness
393
+ // rule: no status, no updated_at.
394
+ async function clearErrorNote(db, id) {
395
+ const { rows } = await db.query(
396
+ `UPDATE provisioning_instances SET error_note = NULL WHERE id = $1 RETURNING id`,
397
+ [id]
398
+ );
399
+ return rows[0] || null;
400
+ }
401
+
375
402
  // The caller's own instances, newest first.
376
403
  async function listInstancesForOwner(db, ownerBuilderId) {
377
404
  const { rows } = await db.query(
@@ -701,7 +728,7 @@ module.exports = {
701
728
  isProvisionedHostname,
702
729
  getInstanceById, getInstanceBySlug, getInstanceByDomain, listInstancesForOwner, listAllInstances,
703
730
  // data access — verification overlay (task 1002696)
704
- listActiveInstancesForLiveness, recordLivenessProbe,
731
+ listActiveInstancesForLiveness, recordLivenessProbe, recordDnsNote, clearErrorNote,
705
732
  createInstance, setInstanceStatus, updateInstanceDomain,
706
733
  enqueueIntent, enqueueTeardown, getOpenIntent, claimNextIntent, resolveIntent,
707
734
  recordEvent, instanceStateForSlug, costLedger,
@@ -589,6 +589,28 @@ module.exports = function provisioningRoutes() {
589
589
  }
590
590
  const current = provisioning.normalizeHostname(inst.domain);
591
591
  if (current && current === domain) {
592
+ // Re-sending the SAME address is the owner's only re-kick when the
593
+ // wiring stopped (task 1002720 / F19) — the mirror of what POST
594
+ // /instances already does for a snagged standup. Only when the wiring
595
+ // DEMONSTRABLY needs it — a retired intent left its error_note, or the
596
+ // probe says the address doesn't answer — and nothing is in flight. A
597
+ // healthy instance (no note, verified up or unprobed) keeps the
598
+ // idempotent no-op below.
599
+ const needsRewire = inst.error_note || inst.last_verified_up === false;
600
+ if (inst.status === 'active' && needsRewire && !(await provisioning.getOpenIntent(pool, id))) {
601
+ const { created } = await provisioning.enqueueIntent(pool, id, 'domain-attach', 'api:self');
602
+ if (created) {
603
+ await provisioning.recordEvent(pool, {
604
+ instanceId: id, ownerBuilderId: req.builder.id, event: 'domain',
605
+ detail: `domain ${domain} re-attach queued (owner re-kick)`, actor: 'api:self',
606
+ }).catch(() => {});
607
+ }
608
+ // created:false = a racing enqueue got there first — still truthful.
609
+ return res.json({ ok: true, changed: false, queued: !!created, domain: inst.domain,
610
+ message: created
611
+ ? 'Wiring that address again — this card updates when it starts.'
612
+ : 'That address is already being wired — this card updates when it starts.' });
613
+ }
592
614
  return res.json({ ok: true, changed: false, queued: false, domain: inst.domain,
593
615
  message: 'This instance already has that address.' });
594
616
  }
@@ -640,8 +662,11 @@ module.exports = function provisioningRoutes() {
640
662
  actor: 'api:self',
641
663
  }).catch(() => {});
642
664
  res.json({ ok: true, changed: true, queued, domain,
665
+ // State-only sentence (task 1002720 / F16): the web tier holds no cloud
666
+ // tokens and cannot know whether a DNS hook exists on the runner, so
667
+ // "it will answer there shortly" was a promise it could not keep.
643
668
  message: queued
644
- ? 'Your instance is being wired to its new address — it will answer there shortly.'
669
+ ? 'Your instance is being wired to its new address — this card will show whether it answers.'
645
670
  : 'Address recorded — it will be wired when the instance is provisioned.' });
646
671
  } catch (err) {
647
672
  console.error('[provisioning] POST /provisioning/instances/:id/domain', err);
@@ -235,5 +235,26 @@ t('publicInstance projects the declared type — nullable, never silently droppe
235
235
  assert.equal(q.type, null, 'a row predating provisioning_007 projects null, not undefined');
236
236
  });
237
237
 
238
+ t('publicInstance projects dns_note — nullable, never silently dropped (task 1002720 / F16)', () => {
239
+ const p = P.publicInstance({ id: 1, slug: 'x', status: 'active', dns_note: 'point the A record at 203.0.113.5' });
240
+ assert.match(p.dns_note, /A record/, 'the owner-facing manual-DNS sentence must reach the card');
241
+ const q = P.publicInstance({ id: 2, slug: 'y', status: 'active' });
242
+ assert.equal(q.dns_note, null, 'a row predating provisioning_008 projects null, not undefined');
243
+ });
244
+
245
+ await ta('recordDnsNote / clearErrorNote are NARROW single-column updates — no status, no updated_at', async () => {
246
+ let sql = '', params = null;
247
+ const db = { query: async (q, p) => { sql = q; params = p; return { rows: [{ id: 5 }] }; } };
248
+ await P.recordDnsNote(db, 5, 'note text');
249
+ assert.match(sql, /SET dns_note = \$2/);
250
+ assert.doesNotMatch(sql, /status|updated_at/, 'the overlay must never reshuffle lifecycle or card order');
251
+ assert.deepEqual(params, [5, 'note text']);
252
+ await P.recordDnsNote(db, 5, null);
253
+ assert.deepEqual(params, [5, null], 'null clears');
254
+ await P.clearErrorNote(db, 5);
255
+ assert.match(sql, /SET error_note = NULL/);
256
+ assert.doesNotMatch(sql, /status\s*=|updated_at/, 'releasing the latch must not touch the lifecycle');
257
+ });
258
+
238
259
  console.log(`\nprovisioning module: ${passed} passed, ${failed} failed`);
239
260
  process.exit(failed ? 1 : 0);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.126",
3
+ "version": "1.19.128",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.126",
9
+ "version": "1.19.128",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.126",
3
+ "version": "1.19.128",
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",
@@ -228,6 +228,17 @@ h1[tabindex="-1"]:focus{outline:none;}
228
228
  .prog.isLive .pnote{color:color-mix(in srgb,var(--ok) 75%,var(--ink));font-size:14px;}
229
229
  .prog.isErr{background:var(--err-soft);border-color:color-mix(in srgb,var(--err) 25%,var(--bg-card));}
230
230
  .prog.isErr .pnote{color:var(--err);}
231
+ /* teardown's calm neutral (F22) — transcribed from cosmos's .strip.flowDown,
232
+ the same .prog-mirrors-.strip duplication as the rest of this block */
233
+ .prog.flowDown{border-color:var(--line);background:linear-gradient(180deg,var(--neutral-soft) 0%,var(--bg-card) 100%);}
234
+ .prog.flowDown .stages li:not(:first-child)::before{background:var(--line);}
235
+ .prog.flowDown .stages li.done:not(:first-child)::before{background:var(--ink-faint);}
236
+ .prog.flowDown .sdot{border-color:var(--line);}
237
+ .prog.flowDown .stages li.done .sdot{border-color:var(--ink-faint);background:var(--ink-faint);}
238
+ .prog.flowDown .stages li.now .sdot{border-color:var(--ink-soft);}
239
+ .prog.flowDown .stages li.now .sdot::after{background:var(--ink-soft);}
240
+ .prog.flowDown .slbl{color:var(--ink-faint);}
241
+ .prog.flowDown .stages li.now .slbl,.prog.flowDown .stages li.done .slbl{color:var(--ink-soft);}
231
242
  .again{
232
243
  display:inline-block;margin-top:14px;font-size:13px;font-weight:700;color:var(--ink-faint);
233
244
  background:none;border:none;cursor:pointer;font-family:var(--font-body);text-decoration:underline;
@@ -2864,25 +2875,55 @@ h1[tabindex="-1"]:focus{outline:none;}
2864
2875
  var watchPending = false; /* a parked progress watch, resumed on re-entry */
2865
2876
  var watchGen = 0; /* same orphaning contract as manifestGen above */
2866
2877
  function stopPolling() { watchGen += 1; if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; } }
2867
- function progressMarkup(stageNow, stagesDone, note, cls) {
2868
- var names = ['Request filed', 'Standing it up', 'Live'];
2869
- return '<div class="prog ' + (cls || '') + '" role="status" aria-live="polite">' +
2870
- '<ul class="stages">' + names.map(function (n, i) {
2878
+ /* The done panel's own flow vocabularies (F22). A local mirror of the mine
2879
+ view's FLOWS — that table lives in the OTHER page IIFE, out of scope here,
2880
+ the same isolation that makes .prog duplicate .strip. */
2881
+ var PROG_FLOWS = {
2882
+ standup: { names: ['Request filed', 'Standing it up', 'Live'], cls: '' },
2883
+ revive: { names: ['Revive queued', 'Standing it up', 'Back live'], cls: '' },
2884
+ teardown: { names: ['Teardown queued', 'Winding down', 'Torn down'], cls: 'flowDown' }
2885
+ };
2886
+ function progressMarkup(stageNow, stagesDone, note, cls, flow) {
2887
+ var f = PROG_FLOWS[flow || 'standup'];
2888
+ return '<div class="prog ' + (f.cls ? f.cls + ' ' : '') + (cls || '') + '" role="status" aria-live="polite">' +
2889
+ '<ul class="stages">' + f.names.map(function (n, i) {
2871
2890
  var c = stagesDone > i ? 'done' : (stageNow === i ? 'now' : '');
2872
2891
  return '<li class="' + c + '"><span class="sdot"></span><span class="slbl">' + n + '</span></li>';
2873
2892
  }).join('') + '</ul>' +
2874
2893
  '<p class="pnote">' + note + '</p></div>';
2875
2894
  }
2876
2895
  /* Renders the current instance state; returns true when the state is terminal
2877
- (no further polling wanted). */
2896
+ (no further polling wanted). The strip is chosen by the OPEN INTENT first,
2897
+ then status (F22): a torn-down instance with a queued revive must never be
2898
+ told to "start another project". */
2878
2899
  function renderProgress(inst) {
2879
2900
  var el = $('liveProgress');
2880
2901
  if (!el) return true;
2881
2902
  var s = inst && inst.status;
2903
+ var revive = !!(inst && inst.torn_down_at);
2904
+ var oi = inst && inst.open_intent;
2905
+ /* requested/provisioning/tearing_down speak their STATUS (a requested
2906
+ instance always has its provision intent open — the intent branch
2907
+ would make the status branch dead and diverge from the mine card) */
2908
+ if (oi && s !== 'provisioning' && s !== 'tearing_down' && s !== 'requested') {
2909
+ if (oi.action === 'provision') {
2910
+ el.innerHTML = progressMarkup(0, 0,
2911
+ (s === 'torn_down' ? 'A revive is queued' : 'A standup is queued') + ' — this panel updates when it starts.',
2912
+ '', s === 'torn_down' ? 'revive' : 'standup');
2913
+ return false;
2914
+ }
2915
+ if (oi.action === 'teardown') {
2916
+ el.innerHTML = progressMarkup(0, 0, 'A teardown is queued — this panel updates when it starts.', '', 'teardown');
2917
+ return false;
2918
+ }
2919
+ }
2882
2920
  if (s === 'requested') {
2883
- el.innerHTML = progressMarkup(1, 1, 'Waiting for a runner to pick it up. This updates the moment it starts.');
2921
+ el.innerHTML = progressMarkup(1, 1, 'Waiting for a runner to pick it up. This updates the moment it starts.', '', revive ? 'revive' : 'standup');
2884
2922
  } else if (s === 'provisioning') {
2885
- el.innerHTML = progressMarkup(1, 1, 'Raising the server, database, and address — nothing needed from you.');
2923
+ el.innerHTML = progressMarkup(1, 1,
2924
+ revive ? 'Bringing it back on the newest platform version — nothing needed from you.'
2925
+ : 'Raising the server, database, and address — nothing needed from you.',
2926
+ '', revive ? 'revive' : 'standup');
2886
2927
  } else if (s === 'active') {
2887
2928
  var addr = inst.domain ? 'https://' + inst.domain : null;
2888
2929
  el.innerHTML = progressMarkup(-1, 3,
@@ -2894,9 +2935,13 @@ h1[tabindex="-1"]:focus{outline:none;}
2894
2935
  false "keeps retrying" — retries stop, and the retry door is the mine view */
2895
2936
  el.innerHTML = progressMarkup(1, 1,
2896
2937
  'The standup stopped partway — it won’t continue on its own. Retry it (and see the technical details) from <a href="?view=mine">your projects page</a>.', 'isErr');
2897
- } else if (s === 'tearing_down' || s === 'torn_down') {
2898
- /* a deliberate end is calm, never styled as a failure (direction §5) */
2899
- el.innerHTML = progressMarkup(-1, 0, 'This instance was torn down. Bring it back any time from <a href="?view=mine">your projects page</a>, or start another below.');
2938
+ } else if (s === 'tearing_down') {
2939
+ /* a deliberate end is calm, never styled as a failure (direction §5) —
2940
+ and it speaks the teardown vocabulary, not the standup's (F22) */
2941
+ el.innerHTML = progressMarkup(1, 1, 'Taking the server offline. The repository stays yours; ' +
2942
+ (inst.hosting_shape === 'dedicated' ? 'its database goes with the machine.' : 'the database is kept.'), '', 'teardown');
2943
+ } else if (s === 'torn_down') {
2944
+ el.innerHTML = progressMarkup(-1, 3, 'This instance was torn down. Bring it back any time from <a href="?view=mine">your projects page</a>, or start another below.', '', 'teardown');
2900
2945
  return true;
2901
2946
  } else {
2902
2947
  el.innerHTML = progressMarkup(0, 1, 'Filed. Watching for the standup to start…');
@@ -2936,6 +2981,7 @@ h1[tabindex="-1"]:focus{outline:none;}
2936
2981
  .then(function (data) {
2937
2982
  if (myGen !== watchGen) return;
2938
2983
  var inst = data.instance;
2984
+ if (inst) inst.open_intent = data.open_intent || null; /* F22: the strip reads intents */
2939
2985
  /* the checklist reflects live state (task 1002694): a status transition —
2940
2986
  e.g. the scaffold landing — re-renders the steps (change-only, so the
2941
2987
  5s tick never clobbers an open dropdown or mid-flight button) */
@@ -3198,6 +3244,10 @@ h1[tabindex="-1"]:focus{outline:none;}
3198
3244
  /* domainless instances get their first address; an assigned default may be
3199
3245
  upgraded to a bring-your-own domain (the server allows exactly that re-point) */
3200
3246
  function canAttachDomain(inst) {
3247
+ /* the re-kick door (F19): an active instance whose address wiring stopped
3248
+ (error_note, no intent in flight) can re-send its own domain — the route
3249
+ re-enqueues the attach for exactly this shape */
3250
+ if (inst.status === 'active' && inst.domain && inst.error_note && !inst.open_intent) return true;
3201
3251
  return (!inst.domain || inst.domain === assignedDefaultFor(inst)) &&
3202
3252
  (inst.status === 'requested' || inst.status === 'error' || inst.status === 'active');
3203
3253
  }
@@ -3258,7 +3308,8 @@ h1[tabindex="-1"]:focus{outline:none;}
3258
3308
  : 'Raising the server, database, and address — nothing needed from you.');
3259
3309
  }
3260
3310
  if (s === 'tearing_down') {
3261
- return stripMarkup('teardown', 1, 1, 'Taking the server offline. The repository stays yours; the database is kept.');
3311
+ return stripMarkup('teardown', 1, 1, 'Taking the server offline. The repository stays yours; ' +
3312
+ (inst.hosting_shape === 'dedicated' ? 'its database goes with the machine.' : 'the database is kept.'));
3262
3313
  }
3263
3314
  if (inst.open_intent) {
3264
3315
  var a = inst.open_intent.action;
@@ -3297,6 +3348,14 @@ h1[tabindex="-1"]:focus{outline:none;}
3297
3348
  /* ── rendering ───────────────────────────────────────────────────────── */
3298
3349
 
3299
3350
  var SHAPE_LABEL = { standalone: 'its own server', 'co-tenant': 'shared server', dedicated: 'its own server' };
3351
+ /* what a teardown ACTUALLY does, per hosting shape (F17 — the DECIDED policy:
3352
+ retention is deliberate on shared boxes; a dedicated machine takes its
3353
+ database with it). Copy follows the runner, never the other way around. */
3354
+ function teardownTruth(inst) {
3355
+ return inst && inst.hosting_shape === 'dedicated'
3356
+ ? 'This project has a machine to itself, so the machine goes away and its database goes with it. Your repository is untouched.'
3357
+ : 'The server goes away and the address stops answering. Your repository is untouched. The database is kept, so bringing it back later restores its memory.';
3358
+ }
3300
3359
 
3301
3360
  function addressRow(inst) {
3302
3361
  if (inst.domain) {
@@ -3305,6 +3364,9 @@ h1[tabindex="-1"]:focus{outline:none;}
3305
3364
  heartbeat earns it — never the runner finishing its script */
3306
3365
  var link = '<a href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener">' + esc(inst.domain) + '</a>';
3307
3366
  if (inst.last_verified_up) return 'Answering at ' + link;
3367
+ /* the runner had no DNS hook (F16): the CAUSE beats the bare symptom —
3368
+ a successful A record flips last_verified_up and retires the note */
3369
+ if (inst.dns_note) return 'Its address is ' + link + '. ' + esc(inst.dns_note);
3308
3370
  if (inst.last_verified_up === false) return 'Not answering at ' + link + ' right now.';
3309
3371
  return 'Its address is ' + link + ' — confirming it answers.';
3310
3372
  }
@@ -3335,20 +3397,28 @@ h1[tabindex="-1"]:focus{outline:none;}
3335
3397
  var addr = addressRow(inst);
3336
3398
  if (addr) body += '<div class="addr">' + addr + '</div>';
3337
3399
  if (canAttachDomain(inst)) {
3400
+ var rekick = inst.status === 'active' && inst.domain && inst.error_note;
3338
3401
  body += '<div class="attach">' +
3339
- '<input type="text" id="dom-' + iid + '" placeholder="myproject.example.com" autocomplete="off" spellcheck="false" maxlength="253">' +
3402
+ '<input type="text" id="dom-' + iid + '" placeholder="myproject.example.com" autocomplete="off" spellcheck="false" maxlength="253"' +
3403
+ (rekick ? ' value="' + esc(inst.domain) + '"' : '') + '>' +
3340
3404
  '<button type="button" class="btn small" data-attach="' + iid + '">' +
3341
- (inst.domain ? 'Use your own domain' : 'Give it an address') + '</button></div>';
3405
+ (rekick ? 'Try the address again' : inst.domain ? 'Use your own domain' : 'Give it an address') + '</button></div>';
3342
3406
  }
3343
- if (inst.status === 'error' && !inst.open_intent) {
3407
+ if ((inst.status === 'error' || inst.error_note) && !inst.open_intent) {
3344
3408
  /* once a retry is queued the strip tells the story — the stale error
3345
- paragraph would just contradict it (owner walk, 2026-08-12) */
3409
+ paragraph would just contradict it (owner walk, 2026-08-12). Also
3410
+ covers an ACTIVE instance whose intent retired with a note (F19) —
3411
+ the work stopped and the card must say so, not stay silent. */
3346
3412
  body += '<div class="projMsg err">' + errMarkup(inst) + '</div>';
3347
3413
  }
3348
3414
  body += stripFor(inst);
3349
- if (gone) {
3415
+ if (gone && !inst.open_intent) {
3416
+ /* with a revive queued, the strip tells the story — this paragraph would
3417
+ contradict it (F22, the same guard the error paragraph has) */
3350
3418
  body += '<div class="projMsg">Torn down' + (inst.torn_down_at ? ' ' + esc(shortDate(inst.torn_down_at)) : '') +
3351
- '. The repository is still yours, and its database is kept — bringing it back takes one click. Or <a href="?view=new">start another</a>.</div>';
3419
+ (inst.hosting_shape === 'dedicated'
3420
+ ? '. The repository is still yours. Bringing it back stands up a fresh machine with an empty database. Or <a href="?view=new">start another</a>.</div>'
3421
+ : '. The repository is still yours, and its database is kept — bringing it back takes one click. Or <a href="?view=new">start another</a>.</div>');
3352
3422
  }
3353
3423
  var lm = lastMsg[inst.id];
3354
3424
  body += '<div class="projMsg' + (lm && lm.cls ? ' ' + lm.cls : '') + '" id="msg-' + iid + '">' + (lm ? lm.text : '') + '</div>';
@@ -3522,7 +3592,7 @@ h1[tabindex="-1"]:focus{outline:none;}
3522
3592
  var act = $('act-' + id);
3523
3593
  if (!act) return;
3524
3594
  act.innerHTML = '<div class="confirmRow">' +
3525
- '<span class="plain">Tear it down? The server goes away and the address stops answering. Your repository is untouched. The database is kept, so bringing it back later restores its memory.</span>' +
3595
+ '<span class="plain">Tear it down? ' + teardownTruth(byId[id]) + '</span>' +
3526
3596
  '<button type="button" class="btn small danger" data-teardown-yes="' + esc(id) + '">Yes, tear it down</button>' +
3527
3597
  '<button type="button" class="linklike" data-teardown-no="' + esc(id) + '">Keep it running</button></div>';
3528
3598
  }