@bongos/core 1.19.263 → 1.19.264

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.263",
6
- "core_contract": "1.19.263",
7
- "source_commit": "37e76e1c5b93e63f78568bd133de202d80718835",
5
+ "core_version": "1.19.264",
6
+ "core_contract": "1.19.264",
7
+ "source_commit": "bf5cc3668f703f2110626798f6c78e6728d46192",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-19T21:43:04.995Z",
9
+ "built_at": "2026-08-20T17:10:47.719Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 318,
13
13
  "agent_docs_stubbed": 22,
14
- "functional_verbatim": 1364,
14
+ "functional_verbatim": 1365,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1704,
20
- "tree_sha256": "1f21bf09538e80b09eeae9562e41106fc7c241e0a80f7bc3db5027dfb9755874",
19
+ "file_count": 1705,
20
+ "tree_sha256": "355cac3de0571ef24bf2a01412c36cef27c3150840aa9f02b8f672a56e7caceb",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -4467,12 +4467,12 @@
4467
4467
  {
4468
4468
  "path": "package-lock.json",
4469
4469
  "mode": "0000644",
4470
- "sha256": "58ecc72707cf6bf7bf66d7f8166ab6ba5aff26889dfa6d16c58d831106e0ee41"
4470
+ "sha256": "da4bc7f536408bd310a6793f1a5fe79c8bd34200116f6654a0c44a757a2eb542"
4471
4471
  },
4472
4472
  {
4473
4473
  "path": "package.json",
4474
4474
  "mode": "0000644",
4475
- "sha256": "b7ba32ed44f54e570d1050b609b36194fd9ae545bad8174dec727f6a8281ea10"
4475
+ "sha256": "f7bd93c3e267ae64b19a7bcbc53f0c34f99e7dc92eb3d09af5692f4ca160d2af"
4476
4476
  },
4477
4477
  {
4478
4478
  "path": "public-docs/index.html",
@@ -5672,7 +5672,7 @@
5672
5672
  {
5673
5673
  "path": "scripts/gds/ship.js",
5674
5674
  "mode": "0000644",
5675
- "sha256": "681de343f8491a2f22165246f21ac371a8f76411b395991c784635e8e9646ed7"
5675
+ "sha256": "542d36dfe7dd25d20936844b237b6681b1e64d34c66448438cf7ef9d86abb79f"
5676
5676
  },
5677
5677
  {
5678
5678
  "path": "scripts/gds/smoke-batch-claim.sh",
@@ -6117,7 +6117,7 @@
6117
6117
  {
6118
6118
  "path": "src/module-api.js",
6119
6119
  "mode": "0000644",
6120
- "sha256": "ac17ec5369c8edf4cb20a63057aec474e85b3253c3e588c859d7d36a756091ab"
6120
+ "sha256": "87b51e22fba9b2f7b17c0628175b359ad761fb916ee6a1c93887ad473d42f588"
6121
6121
  },
6122
6122
  {
6123
6123
  "path": "src/module-loader/catalog.js",
@@ -8249,6 +8249,11 @@
8249
8249
  "mode": "0000644",
8250
8250
  "sha256": "e236bc3d6882bc6ddf5d389371a92a96aec62769f14c37bbca31810bcbea0d61"
8251
8251
  },
8252
+ {
8253
+ "path": "tests/ship_error_shape.mjs",
8254
+ "mode": "0000644",
8255
+ "sha256": "a3b7858c255f1c7ea692866dbadd7321f77bd88c8380c429b7cb5f6b1b65496b"
8256
+ },
8252
8257
  {
8253
8258
  "path": "tests/ship_post_land_verify.mjs",
8254
8259
  "mode": "0000644",
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.263",
3
+ "version": "1.19.264",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.263",
9
+ "version": "1.19.264",
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.263",
3
+ "version": "1.19.264",
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",
@@ -729,7 +729,7 @@ async function uploadSessionOnShip(taskId) {
729
729
  recordUploadSkip(taskId, {
730
730
  code: `http_${r.status}`,
731
731
  reason:
732
- `POST /api/gds/sessions returned ${r.status}${r.data && r.data.error ? ` (${r.data.error})` : ''} — ` +
732
+ `POST /api/gds/sessions returned ${r.status}${r.data && r.data.error ? ` (${apiErrorLine(r.data)})` : ''} — ` +
733
733
  'the API rejected or could not store the digest. This ship will NOT appear in the BFG session corpus.',
734
734
  });
735
735
  }
@@ -1268,7 +1268,7 @@ async function ciLandServer(taskId, branch, valueSummary) {
1268
1268
  id: taskId,
1269
1269
  body: { branch, head_sha: headSha, value_summary: valueSummary, bundle_b64: bundleB64 },
1270
1270
  });
1271
- if (r.status === 403 && r.data && r.data.error === 'box_scope') {
1271
+ if (r.status === 403 && normalizeApiError(r.data).code === 'box_scope') {
1272
1272
  console.log(' ci-land: this session is BOX-SCOPED — run /builder-reauth (browser, no terminal) to mint a real session, then re-ship. Task stays at confirmed.');
1273
1273
  return landBailed('this CLI session is box-scoped', '/builder-reauth');
1274
1274
  }
@@ -3160,9 +3160,39 @@ function normalizeApiError(data) {
3160
3160
  return {
3161
3161
  code: isEnvelope ? errRaw.code : errRaw,
3162
3162
  message: isEnvelope ? errRaw.message : undefined,
3163
+ // task 1002789: `details` is where the envelope carries the structured
3164
+ // payload the flat shape used to put at the TOP level (claimWorktree,
3165
+ // step/reason, floors). A site that reads `data.<field>` directly is
3166
+ // reading a field that moved. Fall back to the body so a legacy flat
3167
+ // response still resolves.
3168
+ details: (isEnvelope && errRaw.details && typeof errRaw.details === 'object')
3169
+ ? errRaw.details
3170
+ : (data && typeof data === 'object' ? data : {}),
3163
3171
  };
3164
3172
  }
3165
3173
 
3174
+ // task 1002789: the ONE renderer for a failed API call on the ship path.
3175
+ // Every generic bail site used to hand the raw body to console.error (or
3176
+ // interpolate `data.error` into a template, which stringifies the ADR 0116
3177
+ // envelope object to the literal "[object Object]"). Both hide a fully
3178
+ // diagnosable server error: console.error's util.inspect stops at depth 2, so
3179
+ // the `details` that names WHICH git step failed prints as "[Object]".
3180
+ // Collapse the body to one readable line instead, whichever shape it is.
3181
+ // Pure + exported for tests/ship_error_shape.mjs.
3182
+ function apiErrorLine(data, status) {
3183
+ const { code, message, details } = normalizeApiError(data);
3184
+ const head = code
3185
+ ? (message && message !== code ? `${code}: ${message}` : code)
3186
+ : (message || (status ? `HTTP ${status}` : 'unknown error'));
3187
+ // Only the small scalar fields — a details blob can carry a whole bundle.
3188
+ const extras = Object.entries(details || {})
3189
+ .filter(([k, v]) => k !== 'code' && k !== 'message' && k !== 'error'
3190
+ && (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean'))
3191
+ .map(([k, v]) => `${k}: ${String(v).replace(/\s+/g, ' ').trim()}`);
3192
+ const line = extras.length ? `${head} (${extras.join('; ')})` : head;
3193
+ return status ? `${line} [HTTP ${status}]` : line;
3194
+ }
3195
+
3166
3196
  // task 1002809: classify a FAILED post-land POST /tasks/<id>/ship. The merge
3167
3197
  // already succeeded, so the server may simply have landed the task first
3168
3198
  // (reconcile-on-read — task 1049 — or the ~5-min reconciler racing this call).
@@ -4068,13 +4098,14 @@ async function shipMain() {
4068
4098
  },
4069
4099
  });
4070
4100
  if (!r.ok) {
4071
- if (r.data?.error === 'CLAIM_SESSION_MISMATCH') {
4072
- const wt = r.data.claimWorktree;
4101
+ const resolveErr = normalizeApiError(r.data);
4102
+ if (resolveErr.code === 'CLAIM_SESSION_MISMATCH') {
4103
+ const wt = resolveErr.details.claimWorktree;
4073
4104
  console.error(`This claim was created in a different session${wt ? ` (worktree: ${wt})` : ''}.`);
4074
4105
  console.error('You can only ship from the session that originally claimed this task.');
4075
4106
  console.error(`To force-release from this session: node scripts/gds/release.js ${taskId} --force`);
4076
4107
  } else {
4077
- console.error(`resolve failed (${r.status}):`, r.data);
4108
+ console.error(`resolve failed: ${apiErrorLine(r.data, r.status)}`);
4078
4109
  }
4079
4110
  process.exit(1);
4080
4111
  }
@@ -4110,7 +4141,7 @@ async function shipMain() {
4110
4141
  // verify ran). --verified is a no-op for every other kind.
4111
4142
  const confirm = await (await cliClient()).tasks.postTasksIdConfirm({ id: taskId, body: { verified } });
4112
4143
  if (!confirm.ok) {
4113
- if (confirm.status === 409 && confirm.data && confirm.data.error === 'verify_attestation_required') {
4144
+ if (confirm.status === 409 && normalizeApiError(confirm.data).code === 'verify_attestation_required') {
4114
4145
  console.error('');
4115
4146
  console.error(`This is a VERIFICATION task — it won't confirm until you attest the verify actually passed.`);
4116
4147
  console.error(`Run the verification, then re-ship with --verified:`);
@@ -4139,7 +4170,7 @@ async function shipMain() {
4139
4170
  console.error(` task.confirm.grade_bypass${floorNote} to POST /api/gds/tasks/${taskId}/confirm.`);
4140
4171
  process.exit(1);
4141
4172
  }
4142
- console.error(`manual-confirm failed (${confirm.status}):`, confirm.data);
4173
+ console.error(`manual-confirm failed: ${apiErrorLine(confirm.data, confirm.status)}`);
4143
4174
  process.exit(1);
4144
4175
  }
4145
4176
  postGradeStatus = confirm.data.taskStatus;
@@ -4490,6 +4521,9 @@ module.exports = {
4490
4521
  // 'pending' (success), not failure. Exported so the semantics are unit-tested.
4491
4522
  classifyLandOutcome,
4492
4523
  normalizeApiError,
4524
+ // task 1002789: the single one-line renderer for a failed API call. Exported
4525
+ // so the "[object Object]" regression is pinned against BOTH wire shapes.
4526
+ apiErrorLine,
4493
4527
  postLandShipOutcome,
4494
4528
  // task 1718: pure poll-path decision — enable-failure polls the SERVER (not a
4495
4529
  // dead-end strand at confirmed), enable-success watches GitHub. Exported so the
package/src/module-api.js CHANGED
@@ -953,8 +953,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
953
953
  // landed since 1.19.261 with no explicit bump. run 32298063660. (task 1002620)
954
954
  // 1.19.263 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
955
955
  // landed since 1.19.262 with no explicit bump. run 32305228786. (task 1002620)
956
+ // 1.19.264 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
957
+ // landed since 1.19.263 with no explicit bump. run 32396082154. (task 1002620)
956
958
  // ---------------------------------------------------------------------------
957
- const CORE_VERSION = '1.19.263'; // CI auto-patch carrier (ADR 0161); see changelog above
959
+ const CORE_VERSION = '1.19.264'; // CI auto-patch carrier (ADR 0161); see changelog above
958
960
 
959
961
  // A namespaced logger so a module's log lines are attributable + consistent.
960
962
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,153 @@
1
+ // tests/ship_error_shape.mjs
2
+ //
3
+ // Unit tests for ship.js's API-error rendering (task 1002789).
4
+ //
5
+ // HIT FOR REAL on task 1002485: a fully diagnosable push failure reached the
6
+ // builder as literally "ci-land: server publish failed - [object Object]".
7
+ // The cause is one bug class repeated at several sites — ship.js read the
8
+ // PRE-ADR-0116 flat body `{ error: 'code', …fields }` while the live server
9
+ // emits the nested envelope `{ error: { code, message, details } }`:
10
+ //
11
+ // * interpolating `data.error` into a template string stringifies the
12
+ // envelope OBJECT — that is the literal "[object Object]";
13
+ // * `data.error === 'some_code'` never matches, so the graceful branch
14
+ // behind it (box_scope re-auth, verify-attestation remedy, the
15
+ // CLAIM_SESSION_MISMATCH explanation) is DEAD CODE against a live server;
16
+ // * `data.<field>` reads a top-level field that MOVED into `error.details`.
17
+ //
18
+ // What we prove here (pure helpers only — no git, no network):
19
+ // 1. normalizeApiError now carries `details`, from the envelope when nested
20
+ // and from the body itself on a legacy flat response.
21
+ // 2. apiErrorLine renders BOTH shapes as one readable line and NEVER emits
22
+ // "[object Object]" — the regression this task exists to kill.
23
+ // 3. It keeps the scalar detail fields that name what actually failed, and
24
+ // drops non-scalar blobs (a details payload can carry a whole bundle).
25
+ // 4. It survives missing/odd/hostile bodies without throwing.
26
+ //
27
+ // Run: node tests/ship_error_shape.mjs
28
+
29
+ import { createRequire } from 'node:module';
30
+
31
+ const require = createRequire(import.meta.url);
32
+ const ship = require('../scripts/gds/ship.js');
33
+ const { normalizeApiError, apiErrorLine } = ship;
34
+
35
+ let passed = 0;
36
+ let failed = 0;
37
+ function test(name, fn) {
38
+ try {
39
+ fn();
40
+ passed++;
41
+ console.log(` ok ${name}`);
42
+ } catch (err) {
43
+ failed++;
44
+ console.log(` FAIL ${name}`);
45
+ console.log(` ${err.message}`);
46
+ }
47
+ }
48
+ function eq(actual, expected, msg) {
49
+ if (actual !== expected) throw new Error(`${msg || 'mismatch'}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
50
+ }
51
+ function has(haystack, needle) {
52
+ if (!String(haystack).includes(needle)) throw new Error(`expected to contain ${JSON.stringify(needle)}, got ${JSON.stringify(haystack)}`);
53
+ }
54
+ function lacks(haystack, needle) {
55
+ if (String(haystack).includes(needle)) throw new Error(`expected NOT to contain ${JSON.stringify(needle)}, got ${JSON.stringify(haystack)}`);
56
+ }
57
+
58
+ // ---- 1. normalizeApiError carries details from both shapes ----
59
+
60
+ test('normalizeApiError takes details from the envelope', () => {
61
+ const { code, details } = normalizeApiError({
62
+ error: { code: 'CLAIM_SESSION_MISMATCH', message: 'other session', details: { claimWorktree: 'wt-1002789' } },
63
+ });
64
+ eq(code, 'CLAIM_SESSION_MISMATCH');
65
+ eq(details.claimWorktree, 'wt-1002789');
66
+ });
67
+
68
+ test('normalizeApiError falls back to the body on the legacy flat shape', () => {
69
+ // The flat shape put the structured fields at the TOP level, so the body IS
70
+ // the details. Both server generations resolve through one read.
71
+ const { code, details } = normalizeApiError({ error: 'CLAIM_SESSION_MISMATCH', claimWorktree: 'wt-legacy' });
72
+ eq(code, 'CLAIM_SESSION_MISMATCH');
73
+ eq(details.claimWorktree, 'wt-legacy');
74
+ });
75
+
76
+ test('normalizeApiError still survives missing/odd bodies', () => {
77
+ eq(normalizeApiError(null).code, undefined);
78
+ eq(typeof normalizeApiError(null).details, 'object');
79
+ eq(normalizeApiError({}).code, undefined);
80
+ });
81
+
82
+ // ---- 2. apiErrorLine never emits "[object Object]" ----
83
+
84
+ test('apiErrorLine renders the ADR 0116 envelope, not [object Object]', () => {
85
+ const line = apiErrorLine({
86
+ error: { code: 'publish_failed', message: 'push rejected', details: { step: 'push', reason: 'non-fast-forward' } },
87
+ }, 500);
88
+ lacks(line, '[object Object]');
89
+ has(line, 'publish_failed');
90
+ has(line, 'push rejected');
91
+ has(line, 'step: push');
92
+ has(line, 'non-fast-forward');
93
+ has(line, 'HTTP 500');
94
+ });
95
+
96
+ test('apiErrorLine renders the legacy flat shape', () => {
97
+ const line = apiErrorLine({ error: 'publish_failed', step: 'push' }, 500);
98
+ lacks(line, '[object Object]');
99
+ has(line, 'publish_failed');
100
+ has(line, 'step: push');
101
+ });
102
+
103
+ test('apiErrorLine does not repeat the code when message just echoes it', () => {
104
+ // envelope() defaults message to the code, so the naive `code: message`
105
+ // join would print "publish_failed: publish_failed".
106
+ const line = apiErrorLine({ error: { code: 'publish_failed', message: 'publish_failed' } }, 500);
107
+ lacks(line, 'publish_failed: publish_failed');
108
+ has(line, 'publish_failed');
109
+ });
110
+
111
+ // ---- 3. scalars kept, blobs dropped ----
112
+
113
+ test('apiErrorLine keeps scalar details and drops blobs', () => {
114
+ const line = apiErrorLine({
115
+ error: {
116
+ code: 'publish_failed',
117
+ message: 'nope',
118
+ details: { step: 'push', attempt: 3, armed: false, bundle: { b64: 'x'.repeat(200) }, files: ['a', 'b'] },
119
+ },
120
+ }, 500);
121
+ has(line, 'step: push');
122
+ has(line, 'attempt: 3');
123
+ has(line, 'armed: false');
124
+ lacks(line, 'bundle');
125
+ lacks(line, 'files');
126
+ lacks(line, '[object Object]');
127
+ });
128
+
129
+ test('apiErrorLine flattens multi-line git stderr onto one line', () => {
130
+ const line = apiErrorLine({
131
+ error: { code: 'publish_failed', message: 'nope', details: { reason: 'error: failed to push\n hint: fetch first\n' } },
132
+ }, 500);
133
+ eq(line.includes('\n'), false, 'must be a single line');
134
+ has(line, 'fetch first');
135
+ });
136
+
137
+ // ---- 4. hostile / empty bodies ----
138
+
139
+ test('apiErrorLine handles a body with no error at all', () => {
140
+ has(apiErrorLine({}, 503), 'HTTP 503');
141
+ has(apiErrorLine(null, 503), 'HTTP 503');
142
+ eq(apiErrorLine(null), 'unknown error');
143
+ });
144
+
145
+ test('apiErrorLine handles a null error member', () => {
146
+ const line = apiErrorLine({ error: null }, 500);
147
+ lacks(line, '[object Object]');
148
+ has(line, 'HTTP 500');
149
+ });
150
+
151
+ console.log('');
152
+ console.log(`ship_error_shape: ${passed} passed, ${failed} failed`);
153
+ process.exit(failed === 0 ? 0 : 1);