@bongos/core 1.19.572 → 1.19.574

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.572",
6
- "core_contract": "1.19.572",
7
- "source_commit": "31894c826b1cb16600f02046b2e17a09f78f3712",
5
+ "core_version": "1.19.574",
6
+ "core_contract": "1.19.574",
7
+ "source_commit": "cb6c20fba8bc93877d7917986195294b3d7d2d42",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-07T00:51:07.099Z",
9
+ "built_at": "2026-09-07T02:14:28.259Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 450,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2042,
14
+ "functional_verbatim": 2043,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2516,
20
- "tree_sha256": "dc4746621684478bdfcc20dba9ccc094ed1722956378f38939e11ff3f83e35d5",
19
+ "file_count": 2517,
20
+ "tree_sha256": "9160b243d293a7be2f13cb02991462b82a800e4e9948486d898f30a7598a0141",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -112,7 +112,7 @@
112
112
  {
113
113
  "path": ".claude/skills/builder-start/SKILL.md",
114
114
  "mode": "0000644",
115
- "sha256": "37b65ec352cb7ca14a117f4e21c7bdbb2808d0ad3af48fdfd1fe3f700778fcc2"
115
+ "sha256": "5f67e62add4e4d2dd1d6cc26702d0b278dda25158a805b98f4da26541e92d749"
116
116
  },
117
117
  {
118
118
  "path": ".claude/skills/builder-sync/SKILL.md",
@@ -2692,7 +2692,7 @@
2692
2692
  {
2693
2693
  "path": "docs/module-api-changelog.md",
2694
2694
  "mode": "0000644",
2695
- "sha256": "d7acc0617ab16007150c0f733248576e5c285286f22b32ec722fa061748994f9"
2695
+ "sha256": "34f3a97a9b2396e37b6b8739a008aa3c79fffa0f7e02df8b8586b755d6cd0206"
2696
2696
  },
2697
2697
  {
2698
2698
  "path": "docs/modules-contract.md",
@@ -4267,7 +4267,7 @@
4267
4267
  {
4268
4268
  "path": "modules/economy/credits.js",
4269
4269
  "mode": "0000644",
4270
- "sha256": "08890c50f8c8b2551a495e8a54e0e4fb2925b928ece5dc9aaf2090ea1c37adb3"
4270
+ "sha256": "ce98912ffa428298ad4fe1a2dbf1d6e1121fb9f966cb3c41092ac40fb1428a43"
4271
4271
  },
4272
4272
  {
4273
4273
  "path": "modules/economy/migrations/economy_001_fix_gds_shipper_description.sql",
@@ -4284,6 +4284,11 @@
4284
4284
  "mode": "0000644",
4285
4285
  "sha256": "cc953acd69f844d158c3a79ea4de3b12d652ea0247c878c6a6c33d5abf68eab3"
4286
4286
  },
4287
+ {
4288
+ "path": "modules/economy/reward-policy.js",
4289
+ "mode": "0000644",
4290
+ "sha256": "e1f432cc9871bcb992face1f8ecd9f63fea17b3b67c121d59e8e28a89ba8d7d5"
4291
+ },
4287
4292
  {
4288
4293
  "path": "modules/economy/reward.js",
4289
4294
  "mode": "0000644",
@@ -7547,12 +7552,12 @@
7547
7552
  {
7548
7553
  "path": "package-lock.json",
7549
7554
  "mode": "0000644",
7550
- "sha256": "439507cc695f5ef93f4fe1aa9a48556b4b9e11a023fa4e60912de1ed8befc5ee"
7555
+ "sha256": "267cb45a286e4a22e781d2c5ff28f5c609683bc79b5d8c327d2b3606aa0795a2"
7551
7556
  },
7552
7557
  {
7553
7558
  "path": "package.json",
7554
7559
  "mode": "0000644",
7555
- "sha256": "bf5fe42d980095754ea4b13501ede3a559376dae4127a99915767ba4be0c27d2"
7560
+ "sha256": "aca40a127da79a5a8217c05ab5c074072dc0840d4b2baf89159d68412cfd6a08"
7556
7561
  },
7557
7562
  {
7558
7563
  "path": "public-docs/index.html",
@@ -7807,7 +7812,7 @@
7807
7812
  {
7808
7813
  "path": "scripts/gds/cli-lib.js",
7809
7814
  "mode": "0000644",
7810
- "sha256": "5aa658b099b2acda06cdb3aecfe905da7de79f2b12c844faca091ff061454017"
7815
+ "sha256": "b08ad47b79417816a9f132e8149f78743eefb47b8824edf3380cdea23447e07f"
7811
7816
  },
7812
7817
  {
7813
7818
  "path": "scripts/gds/client-baseurl-guard.js",
@@ -8782,7 +8787,7 @@
8782
8787
  {
8783
8788
  "path": "scripts/gds/start.js",
8784
8789
  "mode": "0000644",
8785
- "sha256": "cb37801c6ef35a64734af422c9651b287236c4d67f7ed72a63e1cd75a94d8b38"
8790
+ "sha256": "81c60afdec1b7b05babe1eceaad38e0f27b932bef74e9dcb2cd3782c58524e30"
8786
8791
  },
8787
8792
  {
8788
8793
  "path": "scripts/gds/status.js",
@@ -9252,7 +9257,7 @@
9252
9257
  {
9253
9258
  "path": "src/module-api.js",
9254
9259
  "mode": "0000644",
9255
- "sha256": "be71a580784409ac5e4507201fbbf9915e16f0682098e3b9e98ec49a0af98216"
9260
+ "sha256": "8b068b3a2cefb919013cdcc7cf35ec4a84859fe16d6e5cf7723d3434370bfcd5"
9256
9261
  },
9257
9262
  {
9258
9263
  "path": "src/module-loader/catalog.js",
@@ -12302,7 +12307,7 @@
12302
12307
  {
12303
12308
  "path": "tests/start_env_guard.mjs",
12304
12309
  "mode": "0000644",
12305
- "sha256": "ef2cf566dc4fcb71487357b181c28bd9bebe4f0b4ec0800c6b7d33e1bf03a1c5"
12310
+ "sha256": "9b0a7d7aeb761cb6fb0c5e1ae650e0848af12e92887ebd630814b35b434c1f41"
12306
12311
  },
12307
12312
  {
12308
12313
  "path": "tests/start_rebase_warning.mjs",
@@ -31,8 +31,13 @@ Delivery is now deterministic (task 1288). The **card-delivery hook** (`.claude/
31
31
 
32
32
  `/builder-start` is frequently the **first** command run in a freshly-cloned instance, where the local environment isn't set up yet. In that state the script can't produce a card — so **route the builder to the one missing step instead of surfacing a raw error**. Match what you see and relay the single fix, plainly:
33
33
 
34
- - **`bongos: command not found`, a "cannot find module" from `node scripts/gds/start.js`, or you can otherwise tell this is a fresh checkout** (a scaffolded instance whose `node_modules` is absent). The `bongos` CLI ships *inside* the not-yet-installed `@bongos/core` package, so nothing runs until deps are installed this is the #1 fresh-clone wall. Route: run `npm install`, then `bongos dev` (one-command local bring-up), then re-run `/builder-start`. Don't try to hand-run internal scripts; the package isn't unpacked yet.
35
- - **The card says the instance isn't reachable / running.** The local server was never brought up. Route: `bongos dev`, then re-run. (`start.js` now prints this guided card itself on a connection failure — relay it verbatim.)
34
+ > **FIRST, read WHERE THIS INSTANCE'S BOARD LIVES `config/branding.json` `domains.buildersOrigin`.** Every route below branches on it, and getting it wrong is worse than an error. A **remote `https://` origin** means the board is **HOSTED**: it is a running website with the builder's real tasks in it. A **`localhost`** origin means the instance is genuinely local and has to be brought up. Prescribing the local cure on a hosted instance walks the builder through three obedient steps to a board that is not theirs.
35
+
36
+ - **`bongos: command not found`, a "cannot find module" from `node scripts/gds/start.js`, or you can otherwise tell this is a fresh checkout** (a scaffolded instance whose `node_modules` is absent). The `bongos` CLI ships *inside* the not-yet-installed `@bongos/core` package, so nothing runs until deps are installed — this is the #1 fresh-clone wall. Note before prescribing anything: **`@bongos/core` is a PRIVATE package**, so a bare `npm install` **404s on any machine with no `NPM_TOKEN`**. That 404 is the expected tokenless outcome, **not** a yanked or missing version — never go hunting for "a version that still exists" to repoint the dependency at. Route by `buildersOrigin`:
37
+ - **Hosted (remote origin)** — lead with the fact that **nothing needs installing**: the board is a website, so open `<buildersOrigin>/builders` to see and claim work right now. The CLI is an optional second client and needs a token. **Never route to `bongos dev` here** — it stands up a *local* instance with its *own empty database*, so it cannot show the hosted board and quietly answers a different question than the one asked.
38
+ - **Local (`localhost` origin)** — the instance really is local: `npm install`, then `bongos dev` (one-command local bring-up), then re-run `/builder-start`.
39
+ Either way, don't hand-run internal scripts; the package isn't unpacked yet.
40
+ - **The card says the instance isn't reachable / running.** Same branch. **Hosted:** the server is not this machine's to start — check the origin is up (`curl -sI <buildersOrigin>/healthz`) and re-run; `bongos dev` cannot reach it, and starting a local instance instead would hide the outage behind an empty board. **Local:** the server was never brought up — route: `bongos dev`, then re-run. (`start.js` prints a guided card itself on a connection failure — relay it verbatim.)
36
41
  - **Exit code 3 / "Wrong instance — refusing to act".** This machine's signed-in session targets a *different* instance than this project's `config/branding.json`. This is correct-by-design (it prevents writing to the wrong place). Route: run the exact `bongos login <origin>` line the message prints (the origin is right there), then re-run. **Do not** set an env override to force the mismatched instance.
37
42
  - **"no Bongos session"** — the auth case above (`/builder-setup` for a first-timer, `/builder-reauth` for a stale session).
38
43
 
@@ -1593,5 +1593,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1593
1593
  landed since 1.19.570 with no explicit bump. run 34065943703. (task 1002620)
1594
1594
  1.19.572 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1595
1595
  landed since 1.19.571 with no explicit bump. run 34071078624. (task 1002620)
1596
+ 1.19.573 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1597
+ landed since 1.19.572 with no explicit bump. run 34072921884. (task 1002620)
1598
+ 1.19.574 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1599
+ landed since 1.19.573 with no explicit bump. run 34075598213. (task 1002620)
1596
1600
  ---------------------------------------------------------------------------
1597
1601
  ```
@@ -151,7 +151,24 @@ const IDEA_GOVERNANCE_PASS_FLOOR = 30; // owner: floor ~30 — see the note on
151
151
  // these defaults; the pure helper keeps its code defaults so tests stay
152
152
  // deterministic and the call site passes the resolved config. The currency LABEL
153
153
  // is `branding.currency.label`.
154
- const REWARD_MARGIN_PCT = 20; // cost-plus margin, percent DEFAULT (task 1005)
154
+ // task 1003676: the reward POLICY (these constants + rewardPolicy/rewardConfig)
155
+ // moved to ./reward-policy so a CLIENT can resolve it without importing this file,
156
+ // which pulls { pool, branding } from src/module-api and with it the whole server.
157
+ // Re-exported below, so this module is still the one place reward resolution lives
158
+ // (ADR 0146) and every existing caller is unchanged.
159
+ const {
160
+ REWARD_MARGIN_PCT, REWARD_MODE_DEFAULT, REWARD_MODE_COST_PLUS_ONLY, rewardPolicy,
161
+ } = require('./reward-policy');
162
+
163
+ // Per-instance reward policy, resolved over the code defaults. Stays HERE, not in
164
+ // reward-policy.js: the branding read must go through the module port (ADR 0083),
165
+ // and a file the CLI can share may not import the port. Best-effort — a branding
166
+ // hiccup falls back to the defaults so reward accrual never crashes.
167
+ function rewardConfig() {
168
+ let r = {};
169
+ try { r = (branding().reward) || {}; } catch { r = {}; }
170
+ return rewardPolicy(r);
171
+ }
155
172
 
156
173
  // Reward MODE — which reward streams an instance pays (task 1002270, ADR 0146).
157
174
  // The two streams (ADR 0054 "two drachma conventions, one ledger") are:
@@ -170,8 +187,6 @@ const REWARD_MARGIN_PCT = 20; // cost-plus margin, percent — DEFAULT (task 100
170
187
  // else. Cloud Bongos's choice (owner decision, 2026-07-16).
171
188
  // Unknown/garbage mode → treated as the default (pay both): a config typo must
172
189
  // never silently zero out every builder's per-task reward.
173
- const REWARD_MODE_DEFAULT = 'cost-plus-and-estimate';
174
- const REWARD_MODE_COST_PLUS_ONLY = 'cost-plus-only';
175
190
 
176
191
  // Pure policy resolver — maps a raw branding.reward object to the effective reward
177
192
  // policy. Exported so a test can assert the mapping without touching the memoized
@@ -185,24 +200,6 @@ const REWARD_MODE_COST_PLUS_ONLY = 'cost-plus-only';
185
200
  // credit writers (IC2/IC4/IC5) gate on `ideatorCredit`, never on `perTaskCredit`.
186
201
  // An instance opts the ideator lane out with `reward.ideatorCredit: false`;
187
202
  // absent/any-non-false → the default TRUE (a pre-ADR-0172 config keeps paying).
188
- function rewardPolicy(rawReward) {
189
- const r = rawReward || {};
190
- const marginPct = Number.isFinite(Number(r.marginPct)) ? Number(r.marginPct) : REWARD_MARGIN_PCT;
191
- const usdPeg = Number.isFinite(Number(r.usdPeg)) && Number(r.usdPeg) > 0 ? Number(r.usdPeg) : 1;
192
- const mode = r.mode === REWARD_MODE_COST_PLUS_ONLY ? REWARD_MODE_COST_PLUS_ONLY : REWARD_MODE_DEFAULT;
193
- const perTaskCredit = mode !== REWARD_MODE_COST_PLUS_ONLY;
194
- // Default-on, opt-out-only: only an explicit `false` withholds the ideator lane.
195
- const ideatorCredit = r.ideatorCredit !== false;
196
- return { marginPct, usdPeg, mode, perTaskCredit, ideatorCredit };
197
- }
198
-
199
- // Per-instance reward policy, resolved over the code defaults. Best-effort: a
200
- // branding hiccup falls back to the defaults so reward accrual never crashes.
201
- function rewardConfig() {
202
- let r = {};
203
- try { r = (branding().reward) || {}; } catch { r = {}; }
204
- return rewardPolicy(r);
205
- }
206
203
 
207
204
  // Pure helper — exported for tests. Returns { trueCostUsd, marginPct, drachmae,
208
205
  // description }. drachmae = round(cost × (1+margin) × usdPeg); at the 1:1 peg a
@@ -0,0 +1,50 @@
1
+ // modules/economy/reward-policy.js — the reward POLICY shape, and nothing else
2
+ // (task 1003676).
3
+ //
4
+ // What it is: the reward defaults and the PURE function that resolves a branding
5
+ // pack's `reward` block over them — margin, USD peg, mode, whether the per-task
6
+ // credit and the ideator lane are on.
7
+ //
8
+ // Why it is its own file, and why it imports NOTHING: this is the one piece of
9
+ // economy a CLIENT needs. `bongos start` prints a task's credit reward and the
10
+ // wording depends on the mode, so the CLI must resolve the policy. It used to get
11
+ // that by requiring credits.js, which imports { pool, branding } from
12
+ // src/module-api at module scope — so one lazy require of one config reader pulled
13
+ // Postgres, auth and db-kernel into the closure of every CLI subcommand, and that
14
+ // is what made the CLI impossible to publish as a client-only package (goal
15
+ // 1000054) and left a new builder with no private-registry credential unable to
16
+ // claim anything.
17
+ //
18
+ // It takes the raw `reward` block as an ARGUMENT rather than reading branding
19
+ // itself: a module may import only src/module-api (ADR 0083, enforced by
20
+ // fitness.js), so a file that both a module and a client can share has to be free
21
+ // of host reads. Each caller supplies its own — credits.js through the module port,
22
+ // the CLI straight from src/branding. One resolver, two readers; ADR 0146's rule
23
+ // that reward resolution is always economy's still holds, because this file is
24
+ // economy's.
25
+
26
+ // Cost-plus margin, percent — the DEFAULT (task 1005).
27
+ const REWARD_MARGIN_PCT = 20;
28
+ const REWARD_MODE_DEFAULT = 'cost-plus-and-estimate';
29
+ const REWARD_MODE_COST_PLUS_ONLY = 'cost-plus-only';
30
+
31
+ // PURE. Resolves a raw branding `reward` block over the code defaults. Every field
32
+ // is validated rather than trusted: a malformed pack degrades to the defaults
33
+ // instead of poisoning an award.
34
+ function rewardPolicy(rawReward) {
35
+ const r = rawReward || {};
36
+ const marginPct = Number.isFinite(Number(r.marginPct)) ? Number(r.marginPct) : REWARD_MARGIN_PCT;
37
+ const usdPeg = Number.isFinite(Number(r.usdPeg)) && Number(r.usdPeg) > 0 ? Number(r.usdPeg) : 1;
38
+ const mode = r.mode === REWARD_MODE_COST_PLUS_ONLY ? REWARD_MODE_COST_PLUS_ONLY : REWARD_MODE_DEFAULT;
39
+ const perTaskCredit = mode !== REWARD_MODE_COST_PLUS_ONLY;
40
+ // Default-on, opt-out-only: only an explicit `false` withholds the ideator lane.
41
+ const ideatorCredit = r.ideatorCredit !== false;
42
+ return { marginPct, usdPeg, mode, perTaskCredit, ideatorCredit };
43
+ }
44
+
45
+ module.exports = {
46
+ REWARD_MARGIN_PCT,
47
+ REWARD_MODE_DEFAULT,
48
+ REWARD_MODE_COST_PLUS_ONLY,
49
+ rewardPolicy,
50
+ };
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.572",
3
+ "version": "1.19.574",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.572",
9
+ "version": "1.19.574",
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.572",
3
+ "version": "1.19.574",
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",
@@ -82,7 +82,17 @@ function rewardPolicy(fromServer) {
82
82
  return { mode: fromServer.mode, perTaskCredit: fromServer.perTaskCredit !== false };
83
83
  }
84
84
  try {
85
- const p = require('../../modules/economy/credits').rewardConfig();
85
+ // economy's PURE resolver + our own branding read, NOT ./credits (task
86
+ // 1003676): credits.js imports { pool, branding } from src/module-api at
87
+ // module scope, so this one fallback require used to drag Postgres + auth +
88
+ // db-kernel into EVERY CLI subcommand's closure — which is what stopped the
89
+ // CLI shipping as a public, client-only package. Same resolver, same answer,
90
+ // no server. The CLI is not a module, so reading src/branding directly here
91
+ // is allowed where it would not be inside modules/ (ADR 0083).
92
+ const { rewardPolicy: resolve } = require('../../modules/economy/reward-policy');
93
+ let raw = {};
94
+ try { raw = (require('../../src/branding').branding().reward) || {}; } catch (_) { raw = {}; }
95
+ const p = resolve(raw);
86
96
  if (p && typeof p.mode === 'string') return p;
87
97
  } catch (_) { /* fall through */ }
88
98
  return { mode: 'cost-plus-and-estimate', perTaskCredit: true };
@@ -518,14 +518,56 @@ function isConnectionError(err) {
518
518
  return /fetch failed|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|network|socket hang up/i.test(msg);
519
519
  }
520
520
 
521
+ // isLocalOrigin — does this origin name a server THIS MACHINE is supposed to run?
522
+ // The whole shape of the unreachable-instance advice turns on it, so it is its own
523
+ // named predicate rather than an inline regex (task 1003675). Anything that is not
524
+ // recognisably loopback is treated as REMOTE: guessing "local" for a hosted board
525
+ // is the failure this exists to prevent, and the remote branch degrades gracefully
526
+ // (it points at a URL) while the local branch would tell someone to stand up a
527
+ // second, empty instance.
528
+ function isLocalOrigin(origin) {
529
+ if (!origin) return true; // nothing to go on → the old local-first wording
530
+ let host;
531
+ try { host = new URL(origin).hostname.toLowerCase(); } catch (_) { return false; }
532
+ return host === 'localhost' || host.endsWith('.localhost')
533
+ || host === '127.0.0.1' || host.startsWith('127.') || host === '::1' || host === '[::1]';
534
+ }
535
+
521
536
  // The guided "instance isn't running" card. Markdown (never leads with `<`), so
522
- // the builder-start skill relays it verbatim even in --widget mode. Tailors the
523
- // first step to whether deps are installed — a fresh clone needs `npm install`
524
- // before the `bongos` CLI (which ships inside @bongos/core) even exists.
537
+ // the builder-start skill relays it verbatim even in --widget mode.
538
+ //
539
+ // It branches on WHERE THE BOARD LIVES, because the same words are actively
540
+ // harmful in the two cases (task 1003675 — the owner hit this twice on a hosted
541
+ // instance). A HOSTED instance is not this machine's server to start: `bongos dev`
542
+ // would stand up a separate LOCAL instance with its OWN EMPTY DATABASE, so a
543
+ // builder who follows it obediently arrives at a board that is not theirs and the
544
+ // real outage stays hidden behind it. The hosted branch therefore leads with the
545
+ // thing that needs nothing installed — the board is a website — and offers the
546
+ // health probe instead of a bring-up.
547
+ //
548
+ // The local branch is unchanged, and still tailors its first step to whether deps
549
+ // are installed: a fresh clone needs `npm install` before the `bongos` CLI (which
550
+ // ships inside the PRIVATE @bongos/core package) even exists.
525
551
  function instanceNotRunningCard({ origin, depsMissing } = {}) {
526
552
  const out = [];
527
553
  out.push(`**builder-start — this instance isn't reachable yet**`);
528
554
  out.push('');
555
+
556
+ if (!isLocalOrigin(origin)) {
557
+ out.push(`I couldn't reach \`${origin}\`. That instance is **hosted** — it isn't this machine's server to start, so there's nothing to bring up here.`);
558
+ out.push('');
559
+ out.push(`1. **The board is a website, and it needs nothing installed** — open ${origin}/builders to see and claim work right now.`);
560
+ out.push(`2. If that doesn't load either, the instance itself is down: \`curl -sI ${origin}/healthz\` says whether it's answering.`);
561
+ out.push(`3. Once it answers, re-run \`/builder-start\`.`);
562
+ out.push('');
563
+ out.push(`**Don't run \`bongos dev\` for this.** It stands up a *separate local* instance with its own empty database — it cannot show you this board, and it would hide the outage behind an empty one.`);
564
+ if (depsMissing) {
565
+ out.push('');
566
+ out.push(`(The \`bongos\` CLI isn't installed here. It's optional — the website above is the same board. \`@bongos/core\` is a **private** package, so installing it needs an \`NPM_TOKEN\`; a 404 without one is expected, not a missing version.)`);
567
+ }
568
+ return out.join('\n');
569
+ }
570
+
529
571
  out.push(`I couldn't reach the Cloud Bongos instance${origin ? ` at \`${origin}\`` : ''}. On a freshly-cloned project that almost always just means it hasn't been brought up yet. To start it locally:`);
530
572
  out.push('');
531
573
  let n = 1;
@@ -932,4 +974,5 @@ module.exports = {
932
974
  rebaseWarningWidget,
933
975
  isConnectionError,
934
976
  instanceNotRunningCard,
977
+ isLocalOrigin,
935
978
  };
package/src/module-api.js CHANGED
@@ -49,7 +49,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
49
49
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
50
50
  // the entry to that file. Look for a version's history there, not here.
51
51
  // ---------------------------------------------------------------------------
52
- const CORE_VERSION = '1.19.572'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
52
+ const CORE_VERSION = '1.19.574'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
53
53
 
54
54
  // A namespaced logger so a module's log lines are attributable + consistent.
55
55
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -18,7 +18,7 @@ import { strict as assert } from 'node:assert';
18
18
  import { createRequire } from 'node:module';
19
19
 
20
20
  const require = createRequire(import.meta.url);
21
- const { isConnectionError, instanceNotRunningCard } = require('../scripts/gds/start.js');
21
+ const { isConnectionError, instanceNotRunningCard, isLocalOrigin } = require('../scripts/gds/start.js');
22
22
 
23
23
  let passed = 0;
24
24
  let failed = 0;
@@ -63,9 +63,12 @@ t('always points at bongos dev and the re-run', () => {
63
63
  assert.ok(card.includes('localhost:3007'), 'names the unreachable origin when known');
64
64
  });
65
65
 
66
+ // The origin must be LOOPBACK: these assert the LOCAL bring-up card, and since
67
+ // task 1003675 a non-loopback origin selects the hosted card, which deliberately
68
+ // says neither of these things. `http://x` used to work here only by accident.
66
69
  t('mentions npm install ONLY when deps are missing', () => {
67
- const missing = instanceNotRunningCard({ origin: 'http://x', depsMissing: true });
68
- const present = instanceNotRunningCard({ origin: 'http://x', depsMissing: false });
70
+ const missing = instanceNotRunningCard({ origin: 'http://localhost:3007', depsMissing: true });
71
+ const present = instanceNotRunningCard({ origin: 'http://localhost:3007', depsMissing: false });
69
72
  assert.ok(missing.includes('npm install'), 'fresh clone (no deps) → npm install step');
70
73
  assert.ok(!present.includes('npm install'), 'deps present → skip the npm install step');
71
74
  });
@@ -79,9 +82,49 @@ t('is markdown (never leads with `<`) so the skill relays it verbatim', () => {
79
82
  });
80
83
 
81
84
  t('offers the wrong-instance escape hatch (bongos login)', () => {
82
- const card = instanceNotRunningCard({ origin: 'http://x', depsMissing: false });
85
+ const card = instanceNotRunningCard({ origin: 'http://localhost:3007', depsMissing: false });
83
86
  assert.ok(/bongos login/.test(card), 'points at bongos login for a different running instance');
84
87
  });
85
88
 
89
+ // --- the HOSTED branch (task 1003675) --------------------------------------
90
+ // A hosted instance is not this machine's server to start. Telling someone to run
91
+ // `bongos dev` there walks them to a SEPARATE, EMPTY local board — three obedient
92
+ // steps to the wrong destination, with the real outage hidden behind it. The
93
+ // owner hit this twice before it was fixed, so each half is pinned here.
94
+ t('a hosted origin never routes to bongos dev', () => {
95
+ for (const deps of [true, false]) {
96
+ const card = instanceNotRunningCard({ origin: 'https://example-instance.cloudbongos.com', depsMissing: deps });
97
+ assert.ok(!/^\s*\d+\.\s*`bongos dev`/m.test(card), 'must not list `bongos dev` as a step');
98
+ assert.ok(/Don't run `bongos dev`/.test(card), 'must say plainly not to, and why');
99
+ assert.ok(/empty database/.test(card), 'must name the consequence, not just forbid it');
100
+ }
101
+ });
102
+
103
+ t('a hosted origin leads with the board being a website that needs no install', () => {
104
+ const card = instanceNotRunningCard({ origin: 'https://example-instance.cloudbongos.com', depsMissing: true });
105
+ assert.ok(card.includes('https://example-instance.cloudbongos.com/builders'), 'hands over the actual board URL');
106
+ assert.ok(/nothing installed/.test(card), 'says it needs nothing installed');
107
+ assert.ok(/healthz/.test(card), 'offers the health probe instead of a bring-up');
108
+ });
109
+
110
+ t('a hosted origin explains the private-package 404 rather than implying a lost version', () => {
111
+ const card = instanceNotRunningCard({ origin: 'https://example-instance.cloudbongos.com', depsMissing: true });
112
+ assert.ok(/private/i.test(card) && /NPM_TOKEN/.test(card), 'names the real cause of a tokenless install failure');
113
+ assert.ok(/not a missing version/.test(card), 'kills the "go find a version that still exists" wild goose chase');
114
+ });
115
+
116
+ t('isLocalOrigin: loopback is local, everything else is remote', () => {
117
+ for (const o of ['http://localhost:3000', 'http://app.localhost:3000', 'http://127.0.0.1:3000', 'http://[::1]:3000']) {
118
+ assert.equal(isLocalOrigin(o), true, `${o} should be local`);
119
+ }
120
+ for (const o of ['https://example-instance.cloudbongos.com', 'http://x', 'https://10.0.0.5']) {
121
+ assert.equal(isLocalOrigin(o), false, `${o} should be remote`);
122
+ }
123
+ // Unknown → the old local-first wording; unparseable → treated as remote, which
124
+ // degrades to a URL rather than to "stand up a second instance".
125
+ assert.equal(isLocalOrigin(''), true, 'no origin at all → keep the local wording');
126
+ assert.equal(isLocalOrigin('not a url'), false, 'unparseable → remote (fail toward the safer card)');
127
+ });
128
+
86
129
  console.log(`\nstart_env_guard: ${passed} passed, ${failed} failed`);
87
130
  if (failed > 0) process.exitCode = 1;