@bongos/core 1.19.611 → 1.19.612

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.611",
6
- "core_contract": "1.19.611",
7
- "source_commit": "2fe1716bf46fd008eb614d9fb317b0e9ee2874eb",
5
+ "core_version": "1.19.612",
6
+ "core_contract": "1.19.612",
7
+ "source_commit": "40c2360599907381009ddf7b676722ed4df90d10",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-09T03:07:36.926Z",
9
+ "built_at": "2026-09-09T04:22:55.358Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 458,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2562,
20
- "tree_sha256": "c6a3113ed2f20af25a3ebcaee8cb4ff9cd42890d51e890a85c31ddf9d63ff5ea",
20
+ "tree_sha256": "5fd467fedfc4ddaeb527aba61440ecd7f5590a0ac941438cc7e28fc2240b7b3b",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -1807,7 +1807,7 @@
1807
1807
  {
1808
1808
  "path": "docs/adr/0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md",
1809
1809
  "mode": "0000644",
1810
- "sha256": "28f3644d3d9dcd2985b7e3f42cf2135af86203eee35b6e509d1ff98f630f1228"
1810
+ "sha256": "43bf2232da3353ab49bfd956a73d09c5045e5fa155e0004d0855a50587c299cd"
1811
1811
  },
1812
1812
  {
1813
1813
  "path": "docs/adr/0259-a-projects-departure-from-the-public-list-is-public.md",
@@ -2732,7 +2732,7 @@
2732
2732
  {
2733
2733
  "path": "docs/module-api-changelog.md",
2734
2734
  "mode": "0000644",
2735
- "sha256": "4cc20b3afc8ec94a1d53e3d3f746269a1a7def4b6798d81402fe9078c002d3b1"
2735
+ "sha256": "b4249f1559e94a715eff18a2b869b12ffab3a3bda0c99566824933a3450c8688"
2736
2736
  },
2737
2737
  {
2738
2738
  "path": "docs/modules-contract.md",
@@ -7632,12 +7632,12 @@
7632
7632
  {
7633
7633
  "path": "package-lock.json",
7634
7634
  "mode": "0000644",
7635
- "sha256": "73ce6d69717b6ed9e4cf704e12b5578a86a5a9d34baaa1df34df7caa5accbedb"
7635
+ "sha256": "e5976c35d2d34d80cb6beacbeaf14658a71ee0b3be7ece60cb60a48c88a09c4a"
7636
7636
  },
7637
7637
  {
7638
7638
  "path": "package.json",
7639
7639
  "mode": "0000644",
7640
- "sha256": "5ac0729891d64cd779f810b1bf4f98997a7dd8756c8baaa9d82577c7e7e29b60"
7640
+ "sha256": "3281707852a3d8ddb7528ceb04c22b1abc9cc26d0656e502fd55fc3c7342f152"
7641
7641
  },
7642
7642
  {
7643
7643
  "path": "public-docs/index.html",
@@ -7712,7 +7712,7 @@
7712
7712
  {
7713
7713
  "path": "scripts/gds/api.js",
7714
7714
  "mode": "0000644",
7715
- "sha256": "96d6b69e9440dac3ed81c490e460a832d385714de0b379650af80817f89ded0f"
7715
+ "sha256": "a018f4ccc228950065ef0a376445ba6fc79318e451f9a6bc648dc8dd97235b87"
7716
7716
  },
7717
7717
  {
7718
7718
  "path": "scripts/gds/api.ps1",
@@ -7807,7 +7807,7 @@
7807
7807
  {
7808
7808
  "path": "scripts/gds/box-connect-lib.js",
7809
7809
  "mode": "0000644",
7810
- "sha256": "56c6f3eae23d427d9c74763521739bae5b704bcd02ad234e43a5434645657519"
7810
+ "sha256": "dbc4dd22d858a7d17c34ebf211edbd6d885afa39ab7b9e3032a7390cd5526638"
7811
7811
  },
7812
7812
  {
7813
7813
  "path": "scripts/gds/box-infra.js",
@@ -7837,7 +7837,7 @@
7837
7837
  {
7838
7838
  "path": "scripts/gds/build-cli-package.js",
7839
7839
  "mode": "0000644",
7840
- "sha256": "760dbc00a1d8ed5dfa2184e93f9106383bbb36c7fdd973ddfc67b7e03f3bc2f8"
7840
+ "sha256": "81ad7fbc99f5c85219f1385cdd6e6954d27a0e2943f285676f060611a4878f4e"
7841
7841
  },
7842
7842
  {
7843
7843
  "path": "scripts/gds/bump-version.js",
@@ -7897,7 +7897,7 @@
7897
7897
  {
7898
7898
  "path": "scripts/gds/cli-lib.js",
7899
7899
  "mode": "0000644",
7900
- "sha256": "beb9d466f7623cae12788f23d0e30fbf3a520fec769e416ce3ad6458b23ceb2b"
7900
+ "sha256": "44b8c997a25d9fcc68ba573d33391546ff98a871c7d99e9aa6d982c69a703824"
7901
7901
  },
7902
7902
  {
7903
7903
  "path": "scripts/gds/client-baseurl-guard.js",
@@ -8717,7 +8717,7 @@
8717
8717
  {
8718
8718
  "path": "scripts/gds/setup.js",
8719
8719
  "mode": "0000644",
8720
- "sha256": "98671f5666ae6bdafa7691bd519367345772c3041680b59c24ec83c56b99f419"
8720
+ "sha256": "36b5c59af3cde0398e432db37f1cd645aab08ea70b308a5e53671a4fce06f7fe"
8721
8721
  },
8722
8722
  {
8723
8723
  "path": "scripts/gds/shell.js",
@@ -9362,7 +9362,7 @@
9362
9362
  {
9363
9363
  "path": "src/module-api.js",
9364
9364
  "mode": "0000644",
9365
- "sha256": "790abd4adb2065c87aa21a729b69b33d01ed07be1c5c307a865f2f8942cc9d3f"
9365
+ "sha256": "7b843863aec10abe437301fd0b0656d02e651f0ced17d616ccc8457b0dbfca9b"
9366
9366
  },
9367
9367
  {
9368
9368
  "path": "src/module-loader/catalog.js",
@@ -9967,7 +9967,7 @@
9967
9967
  {
9968
9968
  "path": "tests/cli_surface.mjs",
9969
9969
  "mode": "0000644",
9970
- "sha256": "589ecc771ab18829049eabc6183939843072160e8476828e246eadd8ff6f0709"
9970
+ "sha256": "5a8cc9320d66980280965ee34fbb5fc4bbcc30c8ffa860324a99ce0ce4e4d61f"
9971
9971
  },
9972
9972
  {
9973
9973
  "path": "tests/cli_token_reissue.mjs",
@@ -124,6 +124,23 @@ benefit. It gains one when ADR 0099's mirror lands.
124
124
  dependency — a public package that depended on a private one would be uninstallable for
125
125
  everyone.
126
126
  - Task 1002025 is superseded and should be closed against this ADR rather than done.
127
+ - **The journey above is instance-dependent, and 0.1.1 shipped assuming it was not**
128
+ ([task 1003730](https://cloudbongos.com/builders#/task/1003730)). `bongos shell` is the answer
129
+ this package gives to `bongos claim`, but `dev-box` is a **control-plane** module — it
130
+ provisions droplets, so every hosted co-tenant checked (hermeslines-marketing, mercury, demo)
131
+ runs without it while cloudbongos.com has it. On those instances the only route the CLI offered
132
+ to a checkout ended in `HTTP 404: not_found`: a newcomer could list the work and not start it.
133
+ The instance manifest had always said so — `/api/gds/instance` carries `modules.enabled` — and
134
+ no CLI script read it. It now does, **failing open** (a core too old to publish a roster must
135
+ never block a working box), and the dispatcher's checkout-only text presents `bongos shell` as
136
+ conditional rather than promising a box it cannot verify.
137
+ - **A surface conversion is only as complete as the paths it is tested on.** Task 1003680 routed
138
+ `start.js` through `surface.js` and pinned exactly that file, so eleven `/builder-*` lines
139
+ shipped in 0.1.1 — including `requireSession()`, the gate every unauthenticated verb passes
140
+ through, which told terminal users to run `/builder-setup`, and `setup.js`'s completion footer,
141
+ which told a newly-registered builder to run two verbs this package does not have. Several also
142
+ named `node scripts/gds/<x>.js`, the checkout this package exists to avoid needing. The test now
143
+ **runs** the guidance printers under both surfaces rather than scanning one file's source.
127
144
 
128
145
  ## Rejected
129
146
 
@@ -1671,5 +1671,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1671
1671
  landed since 1.19.609 with no explicit bump. run 34305007041. (task 1002620)
1672
1672
  1.19.611 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1673
1673
  landed since 1.19.610 with no explicit bump. run 34305857169. (task 1002620)
1674
+ 1.19.612 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1675
+ landed since 1.19.611 with no explicit bump. run 34310708322. (task 1002620)
1674
1676
  ---------------------------------------------------------------------------
1675
1677
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.611",
3
+ "version": "1.19.612",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.611",
9
+ "version": "1.19.612",
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.611",
3
+ "version": "1.19.612",
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",
@@ -18,7 +18,8 @@
18
18
  // on a 2xx, 1 on any non-2xx or transport error, 2 on bad invocation.
19
19
 
20
20
  const fs = require('node:fs');
21
- const { cliClient, arg, stripBom, resolveQuiet, isBoxScope, printBoxScopeGuidanceAndExit, cliExit, repairMsysMangledPath } = require('./cli-lib');
21
+ const { cliClient, arg, stripBom, resolveQuiet, isBoxScope, printBoxScopeGuidanceAndExit, cliExit, repairMsysMangledPath,
22
+ instanceDevBoxSupport, devBoxUnavailableMessage } = require('./cli-lib');
22
23
 
23
24
 
24
25
  (async () => {
@@ -89,6 +90,14 @@ const { cliClient, arg, stripBom, resolveQuiet, isBoxScope, printBoxScopeGuidanc
89
90
  if (res.data !== undefined && res.data !== null) {
90
91
  console.log(JSON.stringify(res.data, null, 2));
91
92
  }
93
+ // task 1003730: `bongos box` in the public CLI is this passthrough aimed at
94
+ // POST /box/ensure, so on an instance without the dev-box module a builder got a raw
95
+ // 404 body and no idea why. api.js stays generic — it just adds the one explanation
96
+ // that a /box/ 404 always means — and only for a reader who typed a box command.
97
+ if (res.status === 404 && /^\/api\/(?:bongos\/v1|bongos|gds)\/box\b/.test(urlPath)) {
98
+ const { available } = await instanceDevBoxSupport();
99
+ if (!available) console.error(`\n✖ ${devBoxUnavailableMessage()}`);
100
+ }
92
101
  // task 1002734: process.exit() here aborted node 5/5 on Windows —
93
102
  // "Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c"
94
103
  // — AFTER the JSON had already printed, so every caller that checks the exit code
@@ -30,7 +30,7 @@ const path = require('node:path');
30
30
  const crypto = require('node:crypto');
31
31
  const { spawnSync } = require('node:child_process');
32
32
  const ic = require('../../src/instance-config');
33
- const { cliClient } = require('./cli-lib');
33
+ const { cliClient, surfaceAction, assertDevBoxAvailable } = require('./cli-lib');
34
34
 
35
35
  // The box always runs as root and the repo is cloned to /workspace (the devcontainer
36
36
  // standardizes the same path). NOT ~/workspace — root's ~ is /root, which has no clone.
@@ -330,9 +330,9 @@ async function ensureKeyRegistered({ created, keyPath, pubKey, log = console.log
330
330
  log(' ✓ Registered your public key with the dev system.');
331
331
  return { registered: true };
332
332
  }
333
- log(` ! Could not register your key (${res.data?.error || res.status}); if SSH is refused, add it via /builder-box.`);
333
+ log(` ! Could not register your key (${res.data?.error || res.status}); if SSH is refused, add it via ${surfaceAction('box')}.`);
334
334
  } catch (err) {
335
- log(` ! Could not register your key (${err.message}); if SSH is refused, add it via /builder-box.`);
335
+ log(` ! Could not register your key (${err.message}); if SSH is refused, add it via ${surfaceAction('box')}.`);
336
336
  }
337
337
  return { registered: false };
338
338
  }
@@ -349,6 +349,7 @@ function sleep(ms) {
349
349
  // public box object from /box/me ({ state, hostname, ip, host_keys, … }).
350
350
  // Throws on a provision denial (rank/blocked) or a timeout.
351
351
  async function ensureBoxActive({ log = console.log, timeoutMs = 6 * 60 * 1000, intervalMs = 5000, sleepFn = sleep } = {}) {
352
+ await assertDevBoxAvailable();
352
353
  const api = await cliClient();
353
354
  const ensureRes = await api.box.postBoxEnsure();
354
355
  if (ensureRes.status === 403) {
@@ -462,6 +463,9 @@ function pinKnownHosts({ hostname, hostKeys, ip = null, log = console.log }) {
462
463
  // The one-shot "get the box ready + local SSH set up" used by both verbs.
463
464
  // Returns { box, keyPath }.
464
465
  async function prepareConnection({ log = console.log } = {}) {
466
+ // Before touching keys: key registration hits the box API too, so without this the reader gets
467
+ // a puzzling "Could not register your key (not_found)" line before the real explanation.
468
+ await assertDevBoxAvailable();
465
469
  const { keyPath, pubKey, created } = ensureSshKey({ log });
466
470
  await ensureKeyRegistered({ created, keyPath, pubKey, log });
467
471
  const box = await ensureBoxActive({ log });
@@ -35,7 +35,7 @@ const REPO_ROOT = path.join(__dirname, '..', '..');
35
35
 
36
36
  // The package's OWN version — deliberately independent of the core's. The core moves many times
37
37
  // a day (CI auto-patch, ADR 0161); the CLI's public contract should not.
38
- const PACKAGE_VERSION = '0.1.1';
38
+ const PACKAGE_VERSION = '0.1.2';
39
39
  const PACKAGE_NAME = '@cloudbongos/cli';
40
40
 
41
41
  // ── What the package carries ────────────────────────────────────────────────────────────────
@@ -140,6 +140,10 @@ const VERBS = {
140
140
  // Verbs that exist in the full core CLI and are deliberately absent here. The dispatcher prints
141
141
  // the reason and the actual next step, because a newcomer hitting a bare "unknown command" learns
142
142
  // nothing — that silence is the exact failure this whole task is fixing.
143
+ //
144
+ // The next step it names is CONDITIONAL prose, not a promise: dev boxes are a control-plane
145
+ // capability many instances run without (task 1003730). The dispatcher has no session or manifest
146
+ // yet, so it must not claim `bongos shell` will work — that verb makes the determination itself.
143
147
  const CHECKOUT_ONLY = {
144
148
  claim: 'Claiming a task writes code, so it needs a real checkout and a worktree.',
145
149
  ship: 'Shipping grades, merges and deploys from a real checkout.',
@@ -307,10 +311,13 @@ function main(argv) {
307
311
  if (CHECKOUT_ONLY[verb]) {
308
312
  process.stderr.write(
309
313
  'bongos: "' + verb + '" is not in the public CLI.\\n ' + CHECKOUT_ONLY[verb] + '\\n\\n' +
310
- 'To get a checkout without installing anything locally:\\n' +
314
+ 'If this instance offers cloud dev boxes, you can get a checkout without\\n' +
315
+ 'installing anything locally:\\n' +
311
316
  ' bongos shell open a terminal on your cloud dev box\\n' +
312
317
  ' bongos code open VS Code on it over Remote-SSH\\n\\n' +
313
- 'The full CLI (including ' + verb + ') is already installed inside that box.\\n'
318
+ 'Not every instance runs dev boxes. If yours does not, bongos shell says so,\\n' +
319
+ 'and the route is a checkout of the project repo instead. The full CLI\\n' +
320
+ '(including ' + verb + ') is installed in both.\\n'
314
321
  );
315
322
  return 2;
316
323
  }
@@ -363,11 +363,19 @@ function printUnauthGuidanceAndExit() {
363
363
  console.error('');
364
364
  console.error('✖ Your Bongos CLI session is no longer valid.');
365
365
  console.error('');
366
- console.error(' Recover the fast (UI-first) way — run /builder-reauth, or:');
367
- console.error(' node scripts/gds/reauth.js');
368
- console.error(' (opens your Settings page; under "CLI Access" click "Re-issue CLI');
369
- console.error(' token", copy the one-liner, paste it back no terminal Device Flow.)');
370
- console.error(' Fallback (terminal Device Flow): node scripts/gds/setup.js --force');
366
+ if (isTerminalSurface()) {
367
+ // No checkout here, so `node scripts/gds/*.js` is not a path this reader has (task 1003730).
368
+ console.error(` Recover the fast (UI-first) way run ${surfaceAction('reauth')}`);
369
+ console.error(' (opens your Settings page; under "CLI Access" click "Re-issue CLI');
370
+ console.error(' token", copy the one-liner, paste it back — no terminal Device Flow.)');
371
+ console.error(' Fallback (browser sign-in): bongos login <instance-url>');
372
+ } else {
373
+ console.error(' Recover the fast (UI-first) way — run /builder-reauth, or:');
374
+ console.error(' node scripts/gds/reauth.js');
375
+ console.error(' (opens your Settings page; under "CLI Access" click "Re-issue CLI');
376
+ console.error(' token", copy the one-liner, paste it back — no terminal Device Flow.)');
377
+ console.error(' Fallback (terminal Device Flow): node scripts/gds/setup.js --force');
378
+ }
371
379
  console.error('');
372
380
  console.error(' Possible causes:');
373
381
  console.error(' - Your session token expired (24h idle TTL, auto-refreshes on each request)');
@@ -375,7 +383,8 @@ function printUnauthGuidanceAndExit() {
375
383
  console.error(' - A recent deploy applied an auth-changing migration (see deploy banner)');
376
384
  console.error('');
377
385
  console.error(' Once re-authed, re-run whichever command you were trying.');
378
- console.error(' For a full diagnosis, run: node scripts/gds/doctor.js');
386
+ // `doctor` is checkout-only it is not one of the public CLI's verbs (ADR 0258).
387
+ if (!isTerminalSurface()) console.error(' For a full diagnosis, run: node scripts/gds/doctor.js');
379
388
  console.error('');
380
389
  process.exit(2);
381
390
  }
@@ -414,7 +423,7 @@ function printBoxScopeGuidanceAndExit() {
414
423
  console.error(' not claim / ship / grade / etc. (the box holds no full-authority token by');
415
424
  console.error(' design — ADR 0053). Re-auth to a real CLI session, then re-run:');
416
425
  console.error('');
417
- console.error(' /builder-reauth (or: node scripts/gds/reauth.js)');
426
+ console.error(isTerminalSurface() ? ' bongos reauth' : ' /builder-reauth (or: node scripts/gds/reauth.js)');
418
427
  console.error('');
419
428
  console.error(` It opens your Settings page (${hallBase()}/settings) on`);
420
429
  console.error(' YOUR machine — under "CLI Access" click "Re-issue CLI token", copy the one-liner, paste it back.');
@@ -655,13 +664,78 @@ async function cliClient(opts = {}) {
655
664
  return client;
656
665
  }
657
666
 
667
+ // ---- does this instance even HAVE dev boxes? (task 1003730) -----------------
668
+ // The dev-box module is a CONTROL-PLANE capability: it provisions DigitalOcean droplets, so a
669
+ // hosted co-tenant instance runs without it. The instance manifest has always said so —
670
+ // GET /api/gds/instance carries modules.enabled — but no CLI script read it, so `bongos shell`
671
+ // on such an instance died with a bare `POST /box/ensure failed (HTTP 404: not_found)`.
672
+ //
673
+ // That mattered far more than a bad error string: the public CLI (ADR 0258) answers
674
+ // `bongos claim` by pointing at `bongos shell`, so on every dev-box-less instance the ONLY route
675
+ // it offered to a checkout was a dead end — a newcomer could see the work and not start it.
676
+ //
677
+ // Cached per process, and FAILS OPEN: a manifest hiccup must never block a working box.
678
+ let _devBoxCheck = null;
679
+
680
+ // The decision, kept pure so it is testable without a network: does THIS manifest prove the
681
+ // instance has no dev box? Anything short of an explicit module roster answers "assume yes" —
682
+ // an older core, a truncated body or an unreachable manifest must never block a working box.
683
+ function devBoxEnabledInManifest(manifest) {
684
+ const enabled = manifest && manifest.modules && manifest.modules.enabled;
685
+ if (!enabled || typeof enabled !== 'object') return true;
686
+ return enabled['dev-box'] === true;
687
+ }
688
+
689
+ async function instanceDevBoxSupport() {
690
+ if (_devBoxCheck) return _devBoxCheck;
691
+ _devBoxCheck = (async () => {
692
+ try {
693
+ const api = await cliClient();
694
+ const res = await api.instance.getInstance();
695
+ return { available: devBoxEnabledInManifest(res && res.ok ? res.data : null) };
696
+ } catch (_) {
697
+ return { available: true };
698
+ }
699
+ })();
700
+ return _devBoxCheck;
701
+ }
702
+
703
+ // The explanation a reader can act on. Deliberately does NOT name the instance: a co-tenant's
704
+ // manifest currently reports the platform's name rather than its own, so naming it would be wrong
705
+ // more often than helpful.
706
+ function devBoxUnavailableMessage() {
707
+ return (
708
+ `This instance doesn't offer cloud dev boxes.\n` +
709
+ ` It runs without the dev-box module, so there is no box to open a shell on.\n` +
710
+ ` Claiming and shipping happen in a checkout of the project's repo, where the full\n` +
711
+ ` CLI is installed.\n` +
712
+ `\n` +
713
+ ` → Ask the project's owner for repo access, then run ${surfaceAction('claim', 'N')} from that checkout.`
714
+ );
715
+ }
716
+
717
+ // Throw that explanation when this instance has no dev box. Callers run it BEFORE any box call,
718
+ // so nothing prints a confusing intermediate 404 first.
719
+ async function assertDevBoxAvailable() {
720
+ const { available } = await instanceDevBoxSupport();
721
+ if (available) return;
722
+ throw new Error(devBoxUnavailableMessage());
723
+ }
724
+
658
725
  async function requireSession() {
659
726
  const s = await loadSession();
660
727
  if (!s || !s.token) {
661
728
  console.error(`No Bongos session found at ${SESSION_PATH}.`);
662
729
  console.error(`You're not signed in yet — pick the line that fits:`);
663
- console.error(` • First time on the project → run /builder-setup (one-time GitHub sign-in that also registers you)`);
664
- console.error(` • Signed in before / expired run /builder-reauth (fast UI re-issue of your CLI token)`);
730
+ if (isTerminalSurface()) {
731
+ // A terminal reader may have no instance configured at all naming one is the whole point
732
+ // of `login`, and `/builder-*` does not exist outside a Claude Code session (task 1003730).
733
+ console.error(` • First time here → run bongos login <instance-url> (sign in with a browser click)`);
734
+ console.error(` • Signed in before / expired → run bongos reauth (re-issue your CLI token)`);
735
+ } else {
736
+ console.error(` • First time on the project → run /builder-setup (one-time GitHub sign-in that also registers you)`);
737
+ console.error(` • Signed in before / expired → run /builder-reauth (fast UI re-issue of your CLI token)`);
738
+ }
665
739
  process.exit(2);
666
740
  }
667
741
  return s;
@@ -752,11 +826,21 @@ function tableTask(t) {
752
826
  // The nudges name an action the reader can run. Which spelling is right depends on who is
753
827
  // reading — see scripts/gds/surface.js (task 1003680). Fail-open: a missing surface module must
754
828
  // never break a nudge.
829
+ //
830
+ // Both helpers are `function` declarations so they hoist — the guidance printers above (which run
831
+ // far earlier in the file) call them, and moving them up would churn the nudge block they belong to.
755
832
  function surfaceAction(verb, arg) {
756
833
  try { return require('./surface').action(verb, arg); }
757
834
  catch (_) { return `/builder-${verb}${arg == null ? '' : ` ${arg}`}`; }
758
835
  }
759
836
 
837
+ // Is a person at a console reading this, rather than a Claude Code session? Fails open to AGENT,
838
+ // so a missing/broken surface module leaves the agent contract byte-identical (task 1003730).
839
+ function isTerminalSurface() {
840
+ try { return require('./surface').isTerminal(); }
841
+ catch (_) { return false; }
842
+ }
843
+
760
844
  function nudgeActionForStage(stage) {
761
845
  switch (stage) {
762
846
  case 'auth_complete':
@@ -937,6 +1021,12 @@ module.exports = {
937
1021
  _noKeepAliveResetForTest,
938
1022
  cliClient,
939
1023
  requireSession,
1024
+ surfaceAction,
1025
+ isTerminalSurface,
1026
+ devBoxEnabledInManifest,
1027
+ instanceDevBoxSupport,
1028
+ devBoxUnavailableMessage,
1029
+ assertDevBoxAvailable,
940
1030
  printUnauthGuidanceAndExit,
941
1031
  isBoxScope,
942
1032
  printBoxScopeGuidanceAndExit,
@@ -19,7 +19,7 @@
19
19
  // home directory) and prints the builder profile.
20
20
 
21
21
  const readline = require('readline');
22
- const { cliClient, saveSession, SESSION_PATH, loadSession, printOnboardingNudge, API_BASE, hallBase } = require('./cli-lib');
22
+ const { cliClient, saveSession, SESSION_PATH, loadSession, printOnboardingNudge, API_BASE, hallBase, surfaceAction, isTerminalSurface } = require('./cli-lib');
23
23
 
24
24
  // The instance's status dashboard, from branding — never a hardcoded host, so a
25
25
  // non-OTB instance points its builders at ITS OWN dashboard (task 1002428).
@@ -386,8 +386,11 @@ async function finishSetup(builder) {
386
386
  console.log('');
387
387
  console.log('Newcomer path:');
388
388
  console.log(` 1. Choose thy craft: open ${hallBase()} (creative / engineering)`);
389
- console.log(' 2. Survey work: /builder-start');
390
- console.log(' 3. Claim + ship: /builder-claim N → /builder-ship');
389
+ console.log(` 2. Survey work: ${surfaceAction('start')}`);
390
+ console.log(` 3. Claim + ship: ${surfaceAction('claim', 'N')}${surfaceAction('ship')}`);
391
+ // Claiming writes code, so it needs a checkout. In a terminal the public CLI answers
392
+ // `bongos claim` with where to get one; say so here rather than let step 3 look self-serve.
393
+ if (isTerminalSurface()) console.log(' (claim + ship run in a checkout — `bongos claim` shows how to get one)');
391
394
  console.log(' 4. Ship three works and you rise to Thetes — the whole queue opens.');
392
395
  }
393
396
  } catch (_) {
@@ -546,11 +549,11 @@ async function main() {
546
549
  console.error('');
547
550
  console.error('This GitHub account has not been approved yet.');
548
551
  console.error('An Archon must approve your access request before you can enroll.');
549
- console.error(' → Ask an Archon to invite you, then re-run /builder-setup.');
552
+ console.error(` → Ask an Archon to invite you, then re-run ${surfaceAction('setup')}.`);
550
553
  process.exit(1);
551
554
  }
552
555
  if (flow.state !== 'ok') {
553
- console.error(`\nSign-in did not complete (${flow.state}). Re-run /builder-setup.`);
556
+ console.error(`\nSign-in did not complete (${flow.state}). Re-run ${surfaceAction('setup')}.`);
554
557
  process.exit(1);
555
558
  }
556
559
  const { token, builder } = flow;
@@ -629,7 +632,9 @@ async function main() {
629
632
  console.error(`${require('./cli-lib').productName()} admits new builders by invitation: an Archon must`);
630
633
  console.error('approve your access request before you can enroll.');
631
634
  console.error(' → Ask an Archon to invite you / approve your access request,');
632
- console.error(' then re-run /builder-setup (or: node scripts/gds/setup.js).');
635
+ console.error(isTerminalSurface()
636
+ ? ` then re-run ${surfaceAction('setup')}.`
637
+ : ' then re-run /builder-setup (or: node scripts/gds/setup.js).');
633
638
  process.exit(1);
634
639
  }
635
640
  if (!poll.ok) {
package/src/module-api.js CHANGED
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
55
55
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
56
56
  // the entry to that file. Look for a version's history there, not here.
57
57
  // ---------------------------------------------------------------------------
58
- const CORE_VERSION = '1.19.611'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
58
+ const CORE_VERSION = '1.19.612'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
59
59
 
60
60
  // A namespaced logger so a module's log lines are attributable + consistent.
61
61
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -144,3 +144,172 @@ test('the skills still invoke the scripts directly, which is what keeps agents o
144
144
  const skill = fs.readFileSync(path.join(REPO_ROOT, '.claude', 'skills', 'builder-start', 'SKILL.md'), 'utf8');
145
145
  assert.match(skill, /node scripts\/gds\/start\.js/, 'builder-start must call the script directly');
146
146
  });
147
+
148
+ // ── 4. The rest of the shipped CLI, not just start.js (task 1003730) ────────────────────────
149
+ //
150
+ // Section 3 above pinned start.js and nothing else, which is exactly why eleven `/builder-*`
151
+ // lines shipped in @cloudbongos/cli@0.1.1 — including the one EVERY unauthenticated verb prints.
152
+ // A terminal reader was told to run `/builder-setup`, which exists only inside Claude Code.
153
+
154
+ import { spawnSync } from 'node:child_process';
155
+ import fs from 'node:fs';
156
+
157
+ // Every .js the public package ships, read out of the build manifest so a newly-shipped script
158
+ // is covered the day it is added rather than the day someone remembers to list it here.
159
+ function shippedScripts() {
160
+ const src = fs.readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'build-cli-package.js'), 'utf8');
161
+ const block = src.match(/const FILES = \[([\s\S]*?)\n\];/);
162
+ assert.ok(block, 'could not read the FILES manifest out of build-cli-package.js');
163
+ return [...block[1].matchAll(/'([^']+\.js)'/g)].map((m) => m[1]);
164
+ }
165
+
166
+ // Run one cli-lib guidance printer in a child process (they all call process.exit) and return
167
+ // everything it wrote. HOME is pointed at nothing so no real session can satisfy requireSession.
168
+ function runGuidance(fn, env) {
169
+ const code =
170
+ `const l=require(${JSON.stringify(path.join(REPO_ROOT, 'scripts', 'gds', 'cli-lib.js'))});` +
171
+ `Promise.resolve(l[${JSON.stringify(fn)}]()).catch(()=>{});`;
172
+ const r = spawnSync(process.execPath, ['-e', code], {
173
+ encoding: 'utf8',
174
+ env: { ...process.env, ...env, HOME: path.join(REPO_ROOT, 'no-such-home-for-tests') },
175
+ });
176
+ return `${r.stdout || ''}${r.stderr || ''}`;
177
+ }
178
+
179
+ const GUIDANCE = ['printUnauthGuidanceAndExit', 'printBoxScopeGuidanceAndExit', 'requireSession'];
180
+
181
+ test('no guidance path speaks slash commands to a terminal reader', () => {
182
+ for (const fn of GUIDANCE) {
183
+ const out = runGuidance(fn, { BONGOS_SURFACE: 'terminal' });
184
+ const leaks = [...out.matchAll(/\/builder-[a-z-]+/g)].map((m) => m[0]);
185
+ assert.deepEqual(leaks, [], `${fn} leaked to a terminal reader: ${leaks.join(', ')}\n${out}`);
186
+ assert.ok(!/node scripts\/gds\//.test(out), `${fn} told a terminal reader to run a repo script:\n${out}`);
187
+ }
188
+ });
189
+
190
+ test('the agent still gets its slash commands from those same paths', () => {
191
+ // The inverse guard: a blanket rewrite that "fixed" the leak by deleting the slash commands
192
+ // would break the reader they were written for, and nothing else here would notice.
193
+ assert.match(runGuidance('printUnauthGuidanceAndExit', {}), /\/builder-reauth/);
194
+ assert.match(runGuidance('printBoxScopeGuidanceAndExit', {}), /\/builder-reauth/);
195
+ assert.match(runGuidance('requireSession', {}), /\/builder-setup/);
196
+ });
197
+
198
+ test('every shipped script that names a slash command routes it through the surface switch', () => {
199
+ // A `/builder-*` literal is only legitimate inside a surface branch. Rather than parse
200
+ // branches, require that any shipped file printing one also consults the switch — and pin the
201
+ // set, so a NEW unguarded file fails here instead of on a newcomer's laptop.
202
+ const ALLOWED = new Set([
203
+ 'scripts/gds/cli-lib.js', // agent side of the three branches, proven above
204
+ 'scripts/gds/setup.js', // re-run + newcomer-path lines, asserted below
205
+ 'scripts/gds/box-connect-lib.js',// key-registration hint, asserted below
206
+ 'scripts/gds/surface.js', // the switch itself
207
+ 'scripts/gds/start.js', // section 3's documented agent-only exemptions
208
+ ]);
209
+ const offenders = [];
210
+ for (const rel of shippedScripts()) {
211
+ const full = path.join(REPO_ROOT, rel);
212
+ if (!fs.existsSync(full)) continue;
213
+ const code = fs.readFileSync(full, 'utf8')
214
+ .split('\n').filter((l) => !l.trim().startsWith('//')).join('\n');
215
+ // Only lines that actually PRINT — a comment or a require path is not user-facing.
216
+ const prints = code.split('\n').filter((l) => /\/builder-[a-z-]/.test(l) && /console\.(log|error)|log\(/.test(l));
217
+ if (prints.length && !ALLOWED.has(rel)) offenders.push(`${rel}: ${prints[0].trim()}`);
218
+ }
219
+ assert.deepEqual(offenders, [], `shipped scripts print slash commands with no surface switch:\n${offenders.join('\n')}`);
220
+ });
221
+
222
+ test('setup.js routes its newcomer path and re-run lines through the surface', () => {
223
+ const src = fs.readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'setup.js'), 'utf8');
224
+ // The completion footer is the worst possible place for a wrong command: it is the first thing
225
+ // a brand-new builder reads after registering.
226
+ assert.match(src, /Survey work:\s*\$\{surfaceAction\('start'\)\}/, 'the newcomer path must use surfaceAction');
227
+ assert.match(src, /surfaceAction\('claim', 'N'\)/, 'claim step must use surfaceAction');
228
+ assert.ok(!/console\.log\(' 2\. Survey work: \/builder-start'\)/.test(src), 'hard-coded /builder-start left in setup.js');
229
+ assert.match(src, /re-run \$\{surfaceAction\('setup'\)\}/, 'the re-run lines must use surfaceAction');
230
+ });
231
+
232
+ test('box-connect-lib names the box command for the reader it has', () => {
233
+ const src = fs.readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'box-connect-lib.js'), 'utf8');
234
+ assert.ok(!/add it via \/builder-box/.test(src), 'hard-coded /builder-box left in box-connect-lib.js');
235
+ assert.match(src, /add it via \$\{surfaceAction\('box'\)\}/);
236
+ });
237
+
238
+ // ── 5. The dead end: an instance with no dev box (task 1003730) ─────────────────────────────
239
+ //
240
+ // `bongos claim N` refuses and points at `bongos shell`. On an instance whose manifest carries
241
+ // no dev-box module that call 404s, so the public CLI's only offered route to a checkout was a
242
+ // hard error — a newcomer could see the work and not start it. The manifest always said so;
243
+ // nothing read it.
244
+
245
+ const cliLib = require(path.join(REPO_ROOT, 'scripts', 'gds', 'cli-lib.js'));
246
+
247
+ test('the manifest decides, and only an explicit roster can prove absence', () => {
248
+ assert.equal(cliLib.devBoxEnabledInManifest({ modules: { enabled: { 'dev-box': true } } }), true);
249
+ assert.equal(cliLib.devBoxEnabledInManifest({ modules: { enabled: { economy: true } } }), false);
250
+ });
251
+
252
+ test('the check fails OPEN — a hiccup must never block a working box', () => {
253
+ // Every one of these is "I could not tell", and the safe answer to that is to proceed and let
254
+ // the real API call speak. Failing closed here would break `bongos shell` on every instance
255
+ // running a core too old to publish a module roster.
256
+ for (const manifest of [null, undefined, {}, { modules: {} }, { modules: { enabled: null } }, 'nonsense']) {
257
+ assert.equal(cliLib.devBoxEnabledInManifest(manifest), true, `should fail open for ${JSON.stringify(manifest)}`);
258
+ }
259
+ });
260
+
261
+ test('the explanation names a command the reader can actually run', () => {
262
+ // It routes through the same switch as everything else: a terminal reader must not be handed
263
+ // `/builder-claim`, and an agent in a checkout must still get it.
264
+ const prev = process.env.BONGOS_SURFACE;
265
+ try {
266
+ process.env.BONGOS_SURFACE = 'terminal';
267
+ const term = cliLib.devBoxUnavailableMessage();
268
+ assert.ok(!/\/builder-/.test(term), `slash command in the terminal dev-box message: ${term}`);
269
+ assert.match(term, /bongos claim N/);
270
+ assert.match(term, /doesn't offer cloud dev boxes/);
271
+ assert.match(term, /checkout/, 'must name the actual alternative, not just refuse');
272
+
273
+ delete process.env.BONGOS_SURFACE;
274
+ assert.match(cliLib.devBoxUnavailableMessage(), /\/builder-claim N/, 'agents keep the slash command');
275
+ } finally {
276
+ if (prev === undefined) delete process.env.BONGOS_SURFACE;
277
+ else process.env.BONGOS_SURFACE = prev;
278
+ }
279
+ });
280
+
281
+ test('both box entry points check BEFORE they touch the box API', () => {
282
+ // Ordering is the whole point: key registration hits the box API too, so gating only the
283
+ // ensure call still printed "Could not register your key (not_found)" first.
284
+ const src = fs.readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'box-connect-lib.js'), 'utf8');
285
+ assert.match(
286
+ src,
287
+ /async function prepareConnection\([^)]*\) \{[\s\S]{0,400}?await assertDevBoxAvailable\(\);[\s\S]{0,200}?ensureSshKey/,
288
+ 'prepareConnection must assert before ensureSshKey/key registration',
289
+ );
290
+ assert.match(
291
+ src,
292
+ /async function ensureBoxActive\([^)]*\) \{\s*await assertDevBoxAvailable\(\);/,
293
+ 'ensureBoxActive must assert before postBoxEnsure',
294
+ );
295
+ });
296
+
297
+ test('the raw box passthrough explains a 404 too', () => {
298
+ // `bongos box` is api.js aimed at POST /box/ensure, so without this it dumped a bare 404 body.
299
+ const src = fs.readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'api.js'), 'utf8');
300
+ assert.match(src, /instanceDevBoxSupport/, 'api.js must consult the dev-box check');
301
+ assert.match(src, /res\.status === 404/, 'only on a 404');
302
+ assert.match(src, /\\\/box\\b/, 'and only for a box path — api.js stays generic');
303
+ });
304
+
305
+ test('the generated dispatcher does not promise a dev box it cannot verify', () => {
306
+ // The dispatcher has no session or manifest yet, so it must present `bongos shell` as
307
+ // conditional. Stating it flatly is what sent newcomers into the 404.
308
+ const gen = fs.readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'build-cli-package.js'), 'utf8');
309
+ assert.ok(
310
+ !/'To get a checkout without installing anything locally/.test(gen),
311
+ 'the unconditional promise is back in the checkout-only refusal',
312
+ );
313
+ assert.match(gen, /If this instance offers cloud dev boxes/);
314
+ assert.match(gen, /Not every instance runs dev boxes/);
315
+ });