@bongos/core 1.19.834 → 1.19.835

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,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.834",
6
- "core_contract": "1.19.834",
7
- "source_commit": "c7f5d1e6cbf225b7ce9e59bf6f3154125ab3f15b",
5
+ "core_version": "1.19.835",
6
+ "core_contract": "1.19.835",
7
+ "source_commit": "31b17fb901dd65b7c6893f797e935c3b503a0de9",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-17T16:08:57.677Z",
9
+ "built_at": "2026-09-17T16:17:34.798Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 493,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2767,
20
- "tree_sha256": "2981d7c12dcad3c4b86f0b609213d11eed3a70b0e9aa62518c1adc9535ee79fb",
20
+ "tree_sha256": "a2ec29793c06b863434f540138312423bddbe46023b7584468bbbdb7d5dfd2b2",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2562,7 +2562,7 @@
2562
2562
  {
2563
2563
  "path": "docs/module-api-changelog.md",
2564
2564
  "mode": "0000644",
2565
- "sha256": "ccb66f38c502ddcefa3b6f3ba78a2e52e9e997c82ad5453fdd1408c1997b8aaa"
2565
+ "sha256": "6cd1402c15f4a97fe2e7da159ad42f283992d01058781df987f5948c25ff9df4"
2566
2566
  },
2567
2567
  {
2568
2568
  "path": "docs/modules-contract.md",
@@ -7852,12 +7852,12 @@
7852
7852
  {
7853
7853
  "path": "package-lock.json",
7854
7854
  "mode": "0000644",
7855
- "sha256": "b91cd05ca37aecf647568e064603860f0bb149fb79cd1147c3700aa4adf495c5"
7855
+ "sha256": "72ff1b680bbebe422781c8e96ff9da2627b98581a871e0f826907525a0eecf04"
7856
7856
  },
7857
7857
  {
7858
7858
  "path": "package.json",
7859
7859
  "mode": "0000644",
7860
- "sha256": "5111d54c082eb876389b62997fb898a8bdb195fa4e91091992a8f092a13e4b35"
7860
+ "sha256": "95c3f6f48a670b840fbf7b1f05f97b5fa93fa06ed08b6322d766b19e46beeef5"
7861
7861
  },
7862
7862
  {
7863
7863
  "path": "public-docs/index.html",
@@ -8772,12 +8772,12 @@
8772
8772
  {
8773
8773
  "path": "scripts/gds/provision-repo.js",
8774
8774
  "mode": "0000644",
8775
- "sha256": "c5822f3b29f90998c831639df2b0925033f17758cc329609de507f5e6e1ad7c8"
8775
+ "sha256": "c8d4ce3004cafbd1d70ee72845700c95c85da41ced29fe059af333869a75cc03"
8776
8776
  },
8777
8777
  {
8778
8778
  "path": "scripts/gds/provision-teardown.js",
8779
8779
  "mode": "0000644",
8780
- "sha256": "9220387c9c6dd53caf8c78103108e7937e2ececd570e2a4d708134a94237ef98"
8780
+ "sha256": "df4187852cf0f75a12571607af95782d16f1655aa4314152078cb9ee9b3d0f7c"
8781
8781
  },
8782
8782
  {
8783
8783
  "path": "scripts/gds/provision-units.js",
@@ -8787,7 +8787,7 @@
8787
8787
  {
8788
8788
  "path": "scripts/gds/provision.js",
8789
8789
  "mode": "0000644",
8790
- "sha256": "d5a8fdc3b79685826c52c9092a6b512c556f0f992c4fc799f5a8e60012f296ab"
8790
+ "sha256": "4f60207affe476f637b2b2c13c38ab785940deef9bf8b5a8fa43b5d6448a408a"
8791
8791
  },
8792
8792
  {
8793
8793
  "path": "scripts/gds/publish-credential-check.js",
@@ -9777,7 +9777,7 @@
9777
9777
  {
9778
9778
  "path": "src/module-api.js",
9779
9779
  "mode": "0000644",
9780
- "sha256": "cc7c4888b81e294f2ea2a0439e1ca80b6a6cc64a3a68e0c71889aed74efc887c"
9780
+ "sha256": "a5a74874643eb65e4e9727c492b8990e42dd5b28d13c40e5a523abcbc1aa320d"
9781
9781
  },
9782
9782
  {
9783
9783
  "path": "src/module-loader/catalog.js",
@@ -12592,7 +12592,7 @@
12592
12592
  {
12593
12593
  "path": "tests/provision.mjs",
12594
12594
  "mode": "0000644",
12595
- "sha256": "1c580b0f1ebd7eca67e7a603c5e10fe2a987e75f60de514dfaf9ac8aff826a12"
12595
+ "sha256": "56be00457be12f34e12658e032285865a6e3a2997df629e4d8dec3ec78c52b31"
12596
12596
  },
12597
12597
  {
12598
12598
  "path": "tests/provision_disconnect.mjs",
@@ -2142,5 +2142,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2142
2142
  landed since 1.19.832 with no explicit bump. run 35243115057. (task 1002620)
2143
2143
  1.19.834 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2144
2144
  landed since 1.19.833 with no explicit bump. run 35244719662. (task 1002620)
2145
+ 1.19.835 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2146
+ landed since 1.19.834 with no explicit bump. run 35245617627. (task 1002620)
2145
2147
  ---------------------------------------------------------------------------
2146
2148
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.834",
3
+ "version": "1.19.835",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.834",
9
+ "version": "1.19.835",
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.834",
3
+ "version": "1.19.835",
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",
@@ -512,6 +512,8 @@ async function scaffoldStandaloneRepo(inst, deps) {
512
512
  throw new Error(`standalone instance ${inst.slug}: target_ref "${inst.target_ref || '(unset)'}" is not an owner/repo — set it (e.g. <owner>/${inst.slug}) so the scaffold knows which repo to clone + push.`);
513
513
  }
514
514
  if (alreadyScaffolded(root, mode)) {
515
+ const resumed = deps.apply ? await resumeUnpushedScaffold(inst, deps, { root, repo, mode }) : null;
516
+ if (resumed) return resumed;
515
517
  log(` [scaffold] ${root} already scaffolded (mode=${mode}) — skipping`);
516
518
  return { already: true };
517
519
  }
@@ -541,13 +543,8 @@ async function scaffoldStandaloneRepo(inst, deps) {
541
543
  // command or makeExec's env (makeExec surfaces both), and is removed in a `finally` so a
542
544
  // failed git op can't leave it on disk. No token (org-owned repo, or secret-box off) →
543
545
  // fall back to the SSH `origin` remote.
544
- const ownerToken = await resolveOwnerGithubToken(inst, deps, { tag: '[scaffold]' });
545
- // The credential-store file + the credential-LESS URL git resolves against it. The URL
546
- // carries only the `x-access-token` username (no secret), so it is safe in a logged argv.
547
- const credFile = ownerToken ? path.join(os.tmpdir(), `bongos-gitcred-${inst.slug}-${process.pid}`) : null;
548
- const httpsUrl = `https://x-access-token@github.com/${repo.owner}/${repo.name}.git`;
549
- const credOpt = credFile ? `-c credential.helper='store --file=${credFile}' ` : '';
550
- if (credFile) writeFile(credFile, `https://x-access-token:${ownerToken}@github.com\n`, { mode: 0o600 });
546
+ const cred = await scaffoldCredential(inst, repo, deps, writeFile);
547
+ const { credFile, httpsUrl, credOpt } = cred;
551
548
 
552
549
  try {
553
550
  // 1. Materialize the working tree.
@@ -680,13 +677,7 @@ async function scaffoldStandaloneRepo(inst, deps) {
680
677
  // destroying their work. Adopt's commit is a descendant of the tip it cloned, so an
681
678
  // ordinary push fast-forwards; if the remote moved underneath us the rejection is the
682
679
  // correct outcome and a re-run picks up the new tip.
683
- if (credFile) {
684
- exec(`git -C ${root} ${credOpt}push ${httpsUrl} HEAD:main`);
685
- log(` [scaffold] ${mode === 'adopt' ? 'pushed the adopt layer' : 'pushed'} to ${repo.owner}/${repo.name}@main via the owner's GitHub token (task 1002317)`);
686
- } else {
687
- exec(`git -C ${root} push -u origin main`);
688
- log(` [scaffold] pushed ${mode === 'adopt' ? 'the adopt layer' : 'initial scaffold'} to ${repo.owner}/${repo.name}@main (SSH origin)`);
689
- }
680
+ pushScaffold({ exec, log, root, repo, mode, cred });
690
681
  return { ok: true, scaffolded: true, mode };
691
682
  } finally {
692
683
  // Always remove the credential file — a failed clone/push must not leave it on disk.
@@ -694,6 +685,54 @@ async function scaffoldStandaloneRepo(inst, deps) {
694
685
  }
695
686
  }
696
687
 
688
+ // The owner-token credential the scaffold's git network ops ride (rationale at the call site in
689
+ // scaffoldStandaloneRepo). The URL carries only the `x-access-token` username, so it is safe in a
690
+ // logged argv. credFile null → no token → SSH origin fallback; the caller `rm -f`s it in a finally.
691
+ async function scaffoldCredential(inst, repo, deps, writeFile) {
692
+ const ownerToken = await resolveOwnerGithubToken(inst, deps, { tag: '[scaffold]' });
693
+ const credFile = ownerToken ? path.join(os.tmpdir(), `bongos-gitcred-${inst.slug}-${process.pid}`) : null;
694
+ const httpsUrl = `https://x-access-token@github.com/${repo.owner}/${repo.name}.git`;
695
+ const credOpt = credFile ? `-c credential.helper='store --file=${credFile}' ` : '';
696
+ if (credFile) writeFile(credFile, `https://x-access-token:${ownerToken}@github.com\n`, { mode: 0o600 });
697
+ return { credFile, httpsUrl, credOpt };
698
+ }
699
+
700
+ function pushScaffold({ exec, log, root, repo, mode, cred }) {
701
+ if (cred.credFile) {
702
+ exec(`git -C ${root} ${cred.credOpt}push ${cred.httpsUrl} HEAD:main`);
703
+ log(` [scaffold] ${mode === 'adopt' ? 'pushed the adopt layer' : 'pushed'} to ${repo.owner}/${repo.name}@main via the owner's GitHub token (task 1002317)`);
704
+ } else {
705
+ exec(`git -C ${root} push -u origin main`);
706
+ log(` [scaffold] pushed ${mode === 'adopt' ? 'the adopt layer' : 'initial scaffold'} to ${repo.owner}/${repo.name}@main (SSH origin)`);
707
+ }
708
+ }
709
+
710
+ // alreadyScaffolded is a LOCAL marker, and a scaffold whose push failed leaves it behind: the
711
+ // re-run skipped the never-done push, then wedged at step 2's `git pull --ff-only` against the
712
+ // still-empty remote (task 1002718, F12). Only a remote that ANSWERS with no main re-pushes — an
713
+ // unreachable one keeps the old skip, as it can't tell "never pushed" from "offline". Returns
714
+ // null when there is nothing to resume.
715
+ async function resumeUnpushedScaffold(inst, deps, { root, repo, mode }) {
716
+ const exec = deps.controlExec || deps.exec;
717
+ const writeFile = deps.controlWriteFile || deps.writeFile;
718
+ let cred = null;
719
+ let probe = { softFailed: true };
720
+ try {
721
+ // This runs on every re-provision of a LIVE instance, so a probe that cannot even start
722
+ // (token lookup, credential write) must keep the skip — never fail a healthy instance.
723
+ try {
724
+ cred = await scaffoldCredential(inst, repo, deps, writeFile);
725
+ probe = exec(`git -C ${root} ${cred.credOpt}ls-remote --heads ${cred.credFile ? cred.httpsUrl : 'origin'} main`, { allowFail: true });
726
+ } catch { deps.log(` [scaffold] could not probe ${repo.owner}/${repo.name} for main — keeping the skip`); }
727
+ if (probe.softFailed || String(probe.stdout || '').trim()) return null;
728
+ deps.log(` [scaffold] ${root} is scaffolded but ${repo.owner}/${repo.name} has no main — re-pushing the scaffold that never landed`);
729
+ pushScaffold({ exec, log: deps.log, root, repo, mode, cred });
730
+ return { ok: true, resumed: true, mode };
731
+ } finally {
732
+ if (cred && cred.credFile) exec(`rm -f ${cred.credFile}`, { allowFail: true });
733
+ }
734
+ }
735
+
697
736
  // ── private-repo deploy key (ADR 0176, task 1002735; walk finding N4) ────────────
698
737
 
699
738
  function deployKeyTitle(inst) { return `cloudbongos-deploy-${inst.slug}`; }
@@ -43,7 +43,7 @@ async function teardownInstance(inst, deps) {
43
43
  // Left for an operator (data is precious), naming ONLY what this row created —
44
44
  // teardown now takes 'requested'/'error'. `port` + `domain` stay held: ADR 0181.
45
45
  if (inst.db_name) log(` (left for the operator: dropdb ${dbName(inst)}, prune ${CONFIG.backupDir}/${dbName(inst)}-*.sql.gz)`);
46
- } else if (apply && deps.doApi && inst.droplet_id) {
46
+ } else if (apply && inst.droplet_id && deps.doApi) {
47
47
  await deps.doApi.deleteDroplet(inst.droplet_id);
48
48
  }
49
49
  parkAddress(inst, { controlExec, log, apply });
@@ -1393,12 +1393,14 @@ async function main() {
1393
1393
  if (CONFIG.remoteHost && !CONFIG.remoteCwd) { console.error('PROVISION_REMOTE_HOST is set but PROVISION_REMOTE_CHECKOUT is not — name the co-hosting box\'s checkout dir (the old hardcoded default was removed, task 1002756).'); process.exit(2); }
1394
1394
  const exec = makeExec({ apply, remoteHost: CONFIG.remoteHost, remoteCwd: CONFIG.remoteCwd, sshOpts: CONFIG.sshOpts });
1395
1395
  const controlExec = makeExec({ apply });
1396
+ let doApiClient = null;
1396
1397
  const deps = {
1397
1398
  db: pool, provisioning, apply, privileged: CONFIG.privileged, log: console.log,
1398
1399
  exec, controlExec,
1399
1400
  writeFile: makeWriteFile({ apply, privileged: CONFIG.privileged, exec, remoteHost: CONFIG.remoteHost }),
1400
1401
  controlWriteFile: makeWriteFile({ apply, privileged: CONFIG.privileged, exec: controlExec }),
1401
- doApi: DoApi ? new DoApi({ dryRun: !apply, token: CONFIG.doToken, log: (m) => console.log(' ' + m) }) : null,
1402
+ // Built on first READ (task 1002718 F6): an --apply tick whose intents never call DO needs no DO_API_TOKEN.
1403
+ get doApi() { return DoApi ? (doApiClient = doApiClient || new DoApi({ dryRun: !apply, token: CONFIG.doToken, log: (m) => console.log(' ' + m) })) : null; },
1402
1404
  // Cross-site identity (ADR 0141 §4). Null on a non-hub control plane → federation
1403
1405
  // dormant; shouldFederate() then skips and the instance owns its auth (as today).
1404
1406
  platformIdentity, hubKeys,
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.834'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.835'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -566,15 +566,79 @@ await ta('scaffoldStandaloneRepo: idempotent — a repo that already carries pac
566
566
  mkdirSync(join(tmp, 'demo'), { recursive: true });
567
567
  writeFileSync(join(tmp, 'demo', 'package.json'), '{}\n'); // sign of a completed scaffold
568
568
  const { deps, cmds } = recorder({ apply: true });
569
+ deps.exec = (cmd) => { cmds.push(cmd); return { ok: true, stdout: /ls-remote/.test(cmd) ? 'abc123\trefs/heads/main\n' : '' }; };
569
570
  const res = await P.scaffoldStandaloneRepo({ slug: 'demo', hosting_shape: 'standalone', target_ref: 'example-owner/demo' }, deps);
570
571
  assert.equal(res.already, true);
571
- assert.equal(cmds.length, 0, 'no init/build/push when already scaffolded');
572
+ assert.deepEqual(cmds.filter((c) => !/ls-remote/.test(c)), [], 'no init/build/push when already scaffolded AND the remote carries main');
572
573
  } finally {
573
574
  P.CONFIG.standaloneBase = origBase;
574
575
  rmSync(tmp, { recursive: true, force: true });
575
576
  }
576
577
  });
577
578
 
579
+ // F12 (task 1002718): package.json is a LOCAL marker, so a scaffold whose push failed left it
580
+ // behind — the re-run skipped the never-done push and wedged at step 2's `git pull --ff-only`
581
+ // on the empty remote. The runner now asks the remote before skipping.
582
+ async function rerunScaffold({ apply = true, lsRemote, token = null, credWriteThrows = false }) {
583
+ const tmp = mkdtempSync(join(tmpdir(), 'scaffold-resume-'));
584
+ const origBase = P.CONFIG.standaloneBase;
585
+ P.CONFIG.standaloneBase = tmp;
586
+ try {
587
+ mkdirSync(join(tmp, 'demo'), { recursive: true });
588
+ writeFileSync(join(tmp, 'demo', 'package.json'), '{}\n');
589
+ const { deps, cmds } = recorder({ apply });
590
+ const writes = [];
591
+ deps.exec = (cmd) => { cmds.push(cmd); return /ls-remote/.test(cmd) ? lsRemote : { ok: true, stdout: '' }; };
592
+ deps.writeFile = (p, body, opts) => {
593
+ if (credWriteThrows && /bongos-gitcred/.test(p)) throw new Error('EACCES: tmp not writable');
594
+ writes.push({ p, body, opts }); return { ok: true };
595
+ };
596
+ if (token) deps.getGithubToken = async () => ({ token, scope: 'repo' });
597
+ const res = await P.scaffoldStandaloneRepo({ slug: 'demo', hosting_shape: 'standalone', target_ref: 'example-owner/demo', owner_builder_id: 7 }, deps);
598
+ return { res, cmds, writes };
599
+ } finally {
600
+ P.CONFIG.standaloneBase = origBase;
601
+ rmSync(tmp, { recursive: true, force: true });
602
+ }
603
+ }
604
+
605
+ await ta('scaffoldStandaloneRepo: a scaffold whose push never landed RE-PUSHES on re-run (remote has no main) — F12, task 1002718', async () => {
606
+ const { res, cmds } = await rerunScaffold({ lsRemote: { ok: true, stdout: '' } });
607
+ const joined = cmds.join('\n');
608
+ assert.equal(res.resumed, true);
609
+ assert.match(joined, /ls-remote --heads [^\n]*origin main/, 'asks the remote whether main exists');
610
+ assert.match(joined, /git -C [^\n]* push -u origin main/, 're-pushes the scaffold commit');
611
+ assert.ok(!/init\.js|package-core|commit -m|--force/.test(joined), 'does NOT re-scaffold, re-commit or force — only the owed push');
612
+ });
613
+
614
+ await ta('scaffoldStandaloneRepo: the resume push rides the owner token and removes its credential file (F12)', async () => {
615
+ const { cmds, writes } = await rerunScaffold({ lsRemote: { ok: true, stdout: '' }, token: 'gho_RESUME_SECRET' });
616
+ const joined = cmds.join('\n');
617
+ assert.ok(!joined.includes('gho_RESUME_SECRET'), 'token never appears in a command string');
618
+ assert.match(joined, /credential\.helper='store --file=[^']*bongos-gitcred-demo[^']*' ls-remote --heads https:\/\/x-access-token@github\.com\/example-owner\/demo\.git main/);
619
+ assert.match(joined, /credential\.helper='store --file=[^']*bongos-gitcred-demo[^']*' push https:\/\/x-access-token@github\.com\/example-owner\/demo\.git HEAD:main/);
620
+ assert.equal(writes.find((w) => /bongos-gitcred-demo/.test(w.p)).opts.mode, 0o600);
621
+ assert.match(cmds[cmds.length - 1], /rm -f [^\n]*bongos-gitcred-demo/, 'credential file removed last');
622
+ });
623
+
624
+ await ta('scaffoldStandaloneRepo: an UNREACHABLE remote keeps the old skip — only a confirmed-empty remote re-pushes (F12)', async () => {
625
+ const { res, cmds } = await rerunScaffold({ lsRemote: { ok: true, stdout: '', softFailed: true } });
626
+ assert.equal(res.already, true);
627
+ assert.ok(!cmds.some((c) => /push/.test(c)), cmds.join(' | '));
628
+ });
629
+
630
+ await ta('scaffoldStandaloneRepo: a probe that cannot start (credential write throws) keeps the skip — a live instance is never failed by it (F12)', async () => {
631
+ const { res, cmds } = await rerunScaffold({ lsRemote: { ok: true, stdout: '' }, token: 'gho_X', credWriteThrows: true });
632
+ assert.equal(res.already, true);
633
+ assert.ok(!cmds.some((c) => /push|ls-remote/.test(c)), cmds.join(' | '));
634
+ });
635
+
636
+ await ta('scaffoldStandaloneRepo: dry-run of an already-scaffolded repo probes nothing (F12)', async () => {
637
+ const { res, cmds } = await rerunScaffold({ apply: false, lsRemote: { ok: true, stdout: '' } });
638
+ assert.equal(res.already, true);
639
+ assert.deepEqual(cmds, []);
640
+ });
641
+
578
642
  // ── ADR 0134 (task 2090): registry core-install path + fail-soft to vendored ──
579
643
  // Runs the scaffold against a FRESH standaloneBase (no package.json → it proceeds) with
580
644
  // an injected env, and asserts which core-resolution shape it chose from the commands.
@@ -1499,6 +1563,30 @@ await ta('teardownInstance(dedicated): no systemd steps at all, so no reset-fail
1499
1563
  assert.ok(!cmds.some((c) => /reset-failed|daemon-reload|disable --now/.test(c)), cmds.join(' | '));
1500
1564
  });
1501
1565
 
1566
+ // F6 (task 1002718): main() builds deps.doApi on first READ, so a caller must only read it
1567
+ // on a path that really calls DigitalOcean — a throwing getter proves the others never do.
1568
+ function doApiTripwire(deps) {
1569
+ Object.defineProperty(deps, 'doApi', { get() { throw new Error('read deps.doApi on a path that makes no DO call'); } });
1570
+ return deps;
1571
+ }
1572
+
1573
+ await ta('teardownInstance(dedicated, no droplet_id) under --apply never reads deps.doApi (F6)', async () => {
1574
+ const { deps } = recorder({ apply: true });
1575
+ await P.teardownInstance({ ...coTenant, hosting_shape: 'dedicated', droplet_id: null }, doApiTripwire(deps));
1576
+ });
1577
+
1578
+ await ta('provisionInstance(co-tenant) under --apply never reads deps.doApi (F6)', async () => {
1579
+ const { deps } = recorder({ apply: true });
1580
+ const r = await P.provisionInstance(coTenant, doApiTripwire(deps));
1581
+ assert.ok(!/deps\.doApi/.test(String(r && r.error)), String(r && r.error));
1582
+ });
1583
+
1584
+ t('main() constructs the DO client lazily — no DO_API_TOKEN needed until a DO call (F6)', () => {
1585
+ const src = readFileSync(new URL('../scripts/gds/provision.js', import.meta.url), 'utf8');
1586
+ assert.match(src, /get doApi\(\)/, 'deps.doApi is a getter');
1587
+ assert.ok(!/doApi:\s*DoApi \? new DoApi/.test(src), 'no eager construction at deps build');
1588
+ });
1589
+
1502
1590
  await ta('teardownInstance(default): NO sudo (bare, unchanged)', async () => {
1503
1591
  const { deps, cmds } = recorder();
1504
1592
  await P.teardownInstance(coTenant, deps);