@bongos/core 1.19.246 → 1.19.248

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.246",
6
- "core_contract": "1.19.246",
7
- "source_commit": "373c652f8aa8a3236479e5adf2cf25c3dd5bfa95",
5
+ "core_version": "1.19.248",
6
+ "core_contract": "1.19.248",
7
+ "source_commit": "bae0bacddfbacf9522b52c969f70eba97a404999",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-18T23:26:34.642Z",
9
+ "built_at": "2026-08-18T23:41:54.166Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 312,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1329,
14
+ "functional_verbatim": 1330,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1662,
20
- "tree_sha256": "6cd3315e2da628addf8cf208857fb6be2124024c7504f5948eb402f3e59dd09c",
19
+ "file_count": 1663,
20
+ "tree_sha256": "e28385efcc78d3c913d04bce79e44e95394413c2e484ed96abffa7e8269d377c",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -4347,12 +4347,12 @@
4347
4347
  {
4348
4348
  "path": "package-lock.json",
4349
4349
  "mode": "0000644",
4350
- "sha256": "366c99418ef6e6309a86d726f762b53e9cfa16c829ac6192f23deb9ea16638d9"
4350
+ "sha256": "16750ea8ecab7425183daae2d05d8f4aceb3769a03651b812ca520b5e1323801"
4351
4351
  },
4352
4352
  {
4353
4353
  "path": "package.json",
4354
4354
  "mode": "0000644",
4355
- "sha256": "2d61dd3140f0f188d6a928badfb8f7e29498e720ce35ee97174a7c05b5fc3467"
4355
+ "sha256": "2dcfaaa9223a22671c43afcc5f0d3abac0db6d73193fc4a402e6d46bd04c519c"
4356
4356
  },
4357
4357
  {
4358
4358
  "path": "public-docs/index.html",
@@ -4847,7 +4847,7 @@
4847
4847
  {
4848
4848
  "path": "scripts/gds/cli-lib.js",
4849
4849
  "mode": "0000644",
4850
- "sha256": "83d27d14600c4c8ec8912c7ad8ce771e0e34903ccc9b567dcc27cb95aeb3c37d"
4850
+ "sha256": "95574e210b1fd215b10c7664da109b0698b18aa7724daa736bade6d752318f63"
4851
4851
  },
4852
4852
  {
4853
4853
  "path": "scripts/gds/code.js",
@@ -5572,7 +5572,7 @@
5572
5572
  {
5573
5573
  "path": "scripts/gds/start.js",
5574
5574
  "mode": "0000644",
5575
- "sha256": "ecb7f326b9ef642e330b3f1d5dd68cf40ee76e451552291804b4a71a28cfab40"
5575
+ "sha256": "cb37801c6ef35a64734af422c9651b287236c4d67f7ed72a63e1cd75a94d8b38"
5576
5576
  },
5577
5577
  {
5578
5578
  "path": "scripts/gds/status.js",
@@ -5972,7 +5972,7 @@
5972
5972
  {
5973
5973
  "path": "src/module-api.js",
5974
5974
  "mode": "0000644",
5975
- "sha256": "9ab5fbe6939d40d1c90001bbbe4b3f60c03620f43ddef279f199658b591c6ae5"
5975
+ "sha256": "63a931fbe395426e0eaa09e2bb10cbbbdc247de66fcbdd836226dceaa45cac22"
5976
5976
  },
5977
5977
  {
5978
5978
  "path": "src/module-loader/catalog.js",
@@ -6439,6 +6439,11 @@
6439
6439
  "mode": "0000644",
6440
6440
  "sha256": "b9258f690d5dc7c87bcbb12d510b3071a38669777ae9a92a99e683e76ea15e83"
6441
6441
  },
6442
+ {
6443
+ "path": "tests/cli_dispatcher.mjs",
6444
+ "mode": "0000644",
6445
+ "sha256": "3e3e17428a6c8f50ce0512cfacc4e18e0b6a2c506d185fa5f2adf21c9feffe9f"
6446
+ },
6442
6447
  {
6443
6448
  "path": "tests/cli_exit_no_abort.mjs",
6444
6449
  "mode": "0000644",
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.246",
3
+ "version": "1.19.248",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.246",
9
+ "version": "1.19.248",
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.246",
3
+ "version": "1.19.248",
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",
@@ -141,6 +141,49 @@ function noKeepAliveDispatcher() {
141
141
  return _noKeepAliveDispatcher;
142
142
  }
143
143
 
144
+ // task 1002670 — the vendored Agent above is only valid as a `dispatcher` for
145
+ // Node's built-in fetch when the two undici copies speak the same internal
146
+ // handler protocol. Node 26 bundles undici 8 while package.json overrides pin
147
+ // the vendored copy at ^6, so built-in fetch rejects the v6 Agent with
148
+ // InvalidArgumentError('invalid onError method') / UND_ERR_INVALID_ARG *before
149
+ // any network I/O* — and every CLI then misreported a pure client-side bug as
150
+ // "this instance isn't reachable yet". The pairing can't be predicted from
151
+ // version numbers alone (a v6 Agent works fine under Node 24's undici 7), so
152
+ // the selection is empirical: try the vendored Agent, and on the first
153
+ // incompat rejection drop it for the rest of the process and retry the request
154
+ // once on the default dispatcher.
155
+ function isDispatcherIncompatError(err) {
156
+ const seen = new Set();
157
+ for (let e = err; e && typeof e === 'object' && !seen.has(e); e = e.cause) {
158
+ seen.add(e);
159
+ if (e.code === 'UND_ERR_INVALID_ARG' || e.name === 'InvalidArgumentError') return true;
160
+ }
161
+ return false;
162
+ }
163
+
164
+ // Test-only: reset the tried-once singleton so tests/cli_dispatcher.mjs can
165
+ // exercise the selection/fallback from a clean slate regardless of what earlier
166
+ // (real) requests did to it on this Node version.
167
+ function _noKeepAliveResetForTest() {
168
+ _noKeepAliveDispatcher = undefined;
169
+ _noKeepAliveTried = false;
170
+ }
171
+
172
+ async function dispatchedFetch(url, init = {}, fetchImpl = fetch) {
173
+ const injected = init.dispatcher !== undefined && init.dispatcher === _noKeepAliveDispatcher;
174
+ try {
175
+ return await fetchImpl(url, init);
176
+ } catch (err) {
177
+ if (!injected || !isDispatcherIncompatError(err)) throw err;
178
+ // The vendored Agent is incompatible with this Node's built-in fetch.
179
+ // Stop offering it process-wide, then retry on the default dispatcher.
180
+ _noKeepAliveDispatcher = undefined;
181
+ const retry = { ...init };
182
+ delete retry.dispatcher;
183
+ return fetchImpl(url, retry);
184
+ }
185
+ }
186
+
144
187
  // cliExit(code) — the post-request replacement for process.exit(code). Records the
145
188
  // exit status and lets node exit naturally once the loop drains, which is what
146
189
  // keeps the UV_HANDLE_CLOSING abort above from firing. Callers must `return` after
@@ -463,7 +506,7 @@ async function apiCall(method, urlPath, body = null, opts = {}) {
463
506
  // account billing, so identity is not an auth boundary. anthropic_user_id is
464
507
  // still kept in the session as cosmetic attribution; it is simply not
465
508
  // transmitted as a header.
466
- const res = await fetch(`${base}${urlPath}`, {
509
+ const res = await dispatchedFetch(`${base}${urlPath}`, {
467
510
  method,
468
511
  headers,
469
512
  body: body ? JSON.stringify(body) : null,
@@ -531,7 +574,7 @@ async function cliClient(opts = {}) {
531
574
  init = init || {};
532
575
  init.headers = Object.assign({ 'User-Agent': CLI_USER_AGENT }, init.headers);
533
576
  if (init.dispatcher === undefined) init.dispatcher = noKeepAliveDispatcher();
534
- const res = await fetch(url, init);
577
+ const res = await dispatchedFetch(url, init);
535
578
  if (res.status === 401 && !allowUnauthenticated && !_unauthExitInFlight) {
536
579
  _unauthExitInFlight = true;
537
580
  printUnauthGuidanceAndExit();
@@ -817,6 +860,10 @@ module.exports = {
817
860
  readSessionToken,
818
861
  apiCall,
819
862
  cliExit,
863
+ isDispatcherIncompatError,
864
+ dispatchedFetch,
865
+ noKeepAliveDispatcher,
866
+ _noKeepAliveResetForTest,
820
867
  cliClient,
821
868
  requireSession,
822
869
  printUnauthGuidanceAndExit,
@@ -52,7 +52,7 @@
52
52
  //
53
53
  // Returns JSON if --json is passed (for the slash command to parse cleanly).
54
54
 
55
- const { cliClient, requireSession, fmtUsd, printOnboardingNudge, printCostWarning, printNeedsNudge, hallBase, API_BASE, rewardPolicy, costPlusOnlyNote } = require('./cli-lib');
55
+ const { cliClient, requireSession, fmtUsd, printOnboardingNudge, printCostWarning, printNeedsNudge, hallBase, API_BASE, rewardPolicy, costPlusOnlyNote, isDispatcherIncompatError } = require('./cli-lib');
56
56
  const { contextPack, formatContextPackCompact } = require('./context-pack');
57
57
  const { depsFinding } = require('./preflight');
58
58
 
@@ -507,6 +507,10 @@ function rankCrossDisciplineRecommendations(claimable, preferredDisciplines, lim
507
507
  // builder-start skill routes those + the pre-install "bongos not found" case.
508
508
  function isConnectionError(err) {
509
509
  if (!err) return false;
510
+ // task 1002670: a dispatcher/argument bug (e.g. a vendored undici Agent the
511
+ // built-in fetch rejects with UND_ERR_INVALID_ARG) throws before any network
512
+ // I/O. That is a CLIENT bug — never present it as "instance isn't reachable".
513
+ if (isDispatcherIncompatError(err)) return false;
510
514
  const codes = ['ECONNREFUSED', 'ENOTFOUND', 'ECONNRESET', 'EAI_AGAIN', 'ETIMEDOUT', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_SOCKET'];
511
515
  const code = err.code || (err.cause && err.cause.code) || '';
512
516
  if (codes.includes(code)) return true;
package/src/module-api.js CHANGED
@@ -919,8 +919,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
919
919
  // landed since 1.19.244 with no explicit bump. run 32196805853. (task 1002620)
920
920
  // 1.19.246 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
921
921
  // landed since 1.19.245 with no explicit bump. run 32197129803. (task 1002620)
922
+ // 1.19.247 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
923
+ // landed since 1.19.246 with no explicit bump. run 32197488065. (task 1002620)
924
+ // 1.19.248 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
925
+ // landed since 1.19.247 with no explicit bump. run 32198208766. (task 1002620)
922
926
  // ---------------------------------------------------------------------------
923
- const CORE_VERSION = '1.19.246'; // CI auto-patch carrier (ADR 0161); see changelog above
927
+ const CORE_VERSION = '1.19.248'; // CI auto-patch carrier (ADR 0161); see changelog above
924
928
 
925
929
  // A namespaced logger so a module's log lines are attributable + consistent.
926
930
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,133 @@
1
+ // tests/cli_dispatcher.mjs — dispatcher selection/fallback for the CLI fetch path
2
+ // (task 1002670).
3
+ //
4
+ // THE BUG. cli-lib's noKeepAliveDispatcher() builds an Agent from the VENDORED
5
+ // undici (package.json overrides pin ^6) and hands it to Node's BUILT-IN fetch as
6
+ // `dispatcher`. Whether that pairing works is an internal-protocol accident of the
7
+ // two undici versions: it works on Node 24 (built-in undici 7), and on Node 26
8
+ // (built-in undici 8) it throws InvalidArgumentError('invalid onError method') /
9
+ // UND_ERR_INVALID_ARG before any network I/O — and every CLI then misreported the
10
+ // client-side bug as "this instance isn't reachable yet".
11
+ //
12
+ // THE CONTRACT PINNED HERE:
13
+ // 1. isDispatcherIncompatError() recognizes the incompat shapes (top-level and
14
+ // wrapped in `cause`, per built-in fetch's TypeError('fetch failed')) and
15
+ // rejects genuine network-error shapes.
16
+ // 2. dispatchedFetch() retries exactly once on the default dispatcher when the
17
+ // injected vendored Agent is rejected — and does NOT mask other failures or
18
+ // caller-supplied dispatchers.
19
+ // 3. start.js's isConnectionError() returns false for the incompat shapes, so a
20
+ // dispatcher bug can never render the "instance isn't reachable" card.
21
+ // 4. End-to-end on THIS Node: the real built-in fetch + the real vendored Agent
22
+ // reach a live local server (on Node 26 this is the case that used to die).
23
+ //
24
+ // Run: node --test tests/cli_dispatcher.mjs
25
+
26
+ import assert from 'node:assert/strict';
27
+ import { test } from 'node:test';
28
+ import http from 'node:http';
29
+ import { createRequire } from 'node:module';
30
+
31
+ const require = createRequire(import.meta.url);
32
+ const cli = require('../scripts/gds/cli-lib.js');
33
+ const { isConnectionError } = require('../scripts/gds/start.js');
34
+
35
+ function incompatError() {
36
+ const cause = new TypeError('invalid onError method');
37
+ cause.name = 'InvalidArgumentError';
38
+ cause.code = 'UND_ERR_INVALID_ARG';
39
+ return new TypeError('fetch failed', { cause });
40
+ }
41
+
42
+ function connRefusedError() {
43
+ const cause = new Error('connect ECONNREFUSED 127.0.0.1:1');
44
+ cause.code = 'ECONNREFUSED';
45
+ return new TypeError('fetch failed', { cause });
46
+ }
47
+
48
+ test('isDispatcherIncompatError: recognizes UND_ERR_INVALID_ARG wrapped in cause', () => {
49
+ assert.equal(cli.isDispatcherIncompatError(incompatError()), true);
50
+ });
51
+
52
+ test('isDispatcherIncompatError: recognizes a bare InvalidArgumentError (no wrapper)', () => {
53
+ const err = new TypeError('invalid onError method');
54
+ err.name = 'InvalidArgumentError';
55
+ assert.equal(cli.isDispatcherIncompatError(err), true);
56
+ });
57
+
58
+ test('isDispatcherIncompatError: rejects genuine network errors', () => {
59
+ assert.equal(cli.isDispatcherIncompatError(connRefusedError()), false);
60
+ assert.equal(cli.isDispatcherIncompatError(new Error('socket hang up')), false);
61
+ assert.equal(cli.isDispatcherIncompatError(null), false);
62
+ });
63
+
64
+ test('isDispatcherIncompatError: survives a self-referential cause chain', () => {
65
+ const err = new Error('loop');
66
+ err.cause = err;
67
+ assert.equal(cli.isDispatcherIncompatError(err), false);
68
+ });
69
+
70
+ test('end-to-end on this Node: real fetch + vendored Agent reach a live server', async () => {
71
+ const server = http.createServer((req, res) => {
72
+ res.setHeader('content-type', 'application/json');
73
+ res.end(JSON.stringify({ ok: true, path: req.url }));
74
+ });
75
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
76
+ const { port } = server.address();
77
+ try {
78
+ // Exactly what apiCall does: built-in fetch, vendored no-keep-alive Agent
79
+ // injected. On Node 26 without the fallback this rejected before any I/O.
80
+ const res = await cli.dispatchedFetch(`http://127.0.0.1:${port}/api/gds/me`, {
81
+ method: 'GET',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ dispatcher: cli.noKeepAliveDispatcher(),
84
+ });
85
+ assert.equal(res.status, 200);
86
+ const data = await res.json();
87
+ assert.equal(data.ok, true);
88
+ } finally {
89
+ server.close();
90
+ }
91
+ });
92
+
93
+ // NOTE: on Node 26 the end-to-end test above already fired the fallback and
94
+ // dropped the singleton, so each test below resets it to a clean slate.
95
+
96
+ test('dispatchedFetch: retries once without dispatcher when the vendored Agent is rejected', async () => {
97
+ cli._noKeepAliveResetForTest();
98
+ const vendored = cli.noKeepAliveDispatcher();
99
+ assert.ok(vendored !== undefined, 'vendored undici Agent should resolve in this repo');
100
+
101
+ const calls = [];
102
+ const fakeFetch = async (url, init) => {
103
+ calls.push(init.dispatcher);
104
+ if (init.dispatcher !== undefined) throw incompatError();
105
+ return { ok: true, status: 200 };
106
+ };
107
+ const res = await cli.dispatchedFetch('http://x/', { dispatcher: vendored }, fakeFetch);
108
+ assert.equal(res.status, 200);
109
+ assert.deepEqual(calls, [vendored, undefined]);
110
+
111
+ // After one incompat rejection the broken Agent is dropped process-wide:
112
+ // later calls get the default dispatcher instead of retrying the broken one.
113
+ assert.equal(cli.noKeepAliveDispatcher(), undefined);
114
+ });
115
+
116
+ test('dispatchedFetch: does not retry a caller-supplied dispatcher, or non-incompat errors', async () => {
117
+ const foreign = { name: 'someone-elses-dispatcher' };
118
+ let calls = 0;
119
+ const rejectingFetch = async () => { calls++; throw incompatError(); };
120
+ await assert.rejects(() => cli.dispatchedFetch('http://x/', { dispatcher: foreign }, rejectingFetch));
121
+ assert.equal(calls, 1);
122
+
123
+ calls = 0;
124
+ const networkFail = async () => { calls++; throw connRefusedError(); };
125
+ await assert.rejects(() => cli.dispatchedFetch('http://x/', {}, networkFail), /fetch failed/);
126
+ assert.equal(calls, 1);
127
+ });
128
+
129
+ test('start.js isConnectionError: dispatcher bug is NOT a connection error', () => {
130
+ assert.equal(isConnectionError(incompatError()), false);
131
+ assert.equal(isConnectionError(connRefusedError()), true);
132
+ assert.equal(isConnectionError(new Error('getaddrinfo ENOTFOUND nope.example')), true);
133
+ });