@bongos/core 1.19.348 → 1.19.349

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.348",
6
- "core_contract": "1.19.348",
7
- "source_commit": "d42db56154d01be276f13e04e364e7ced8d8c400",
5
+ "core_version": "1.19.349",
6
+ "core_contract": "1.19.349",
7
+ "source_commit": "e54f95adb8fcdf1913b4594cffacffd27ad8589b",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-25T02:48:07.802Z",
9
+ "built_at": "2026-08-25T02:56:45.668Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 328,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 1849,
20
- "tree_sha256": "3004531bfb70cd33dbca752e2d945c4687d6ea1148f54548b6fcf921f953681f",
20
+ "tree_sha256": "8c67ac08217f3c2062ec02b6f17c7db9d813a2ac3aafb642fefca9ca3830f579",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -3997,7 +3997,7 @@
3997
3997
  {
3998
3998
  "path": "modules/lifecycle/github-push.js",
3999
3999
  "mode": "0000644",
4000
- "sha256": "127b6666bfbc4b85baebe85b0bf5501ad73b517faf4dafbc1a76a5535ac04ebc"
4000
+ "sha256": "d50b04376a9725bae0f8fe9c81166224eb47242655fec49f33f89b0f87b555f0"
4001
4001
  },
4002
4002
  {
4003
4003
  "path": "modules/lifecycle/goal-advisory.js",
@@ -4132,7 +4132,7 @@
4132
4132
  {
4133
4133
  "path": "modules/lifecycle/routes/tasks.js",
4134
4134
  "mode": "0000644",
4135
- "sha256": "07a56f93428baf4d8cbf7a69a76c052e706ec7759134fcf3db153a37c9ab575c"
4135
+ "sha256": "b29d9bffce9c81e0f5e02ea2da387e15dc6f0bf8d96820931c29275861eeaf66"
4136
4136
  },
4137
4137
  {
4138
4138
  "path": "modules/lifecycle/routes/versions.js",
@@ -4762,12 +4762,12 @@
4762
4762
  {
4763
4763
  "path": "package-lock.json",
4764
4764
  "mode": "0000644",
4765
- "sha256": "6745e4e181b20731968d02f8e4dac7cb864acbf3fbbd6ec6054c799a41487618"
4765
+ "sha256": "e4dcce1602dec36614f7978f6140d1022f23cb4d52f043d4fe4c1b065641dc39"
4766
4766
  },
4767
4767
  {
4768
4768
  "path": "package.json",
4769
4769
  "mode": "0000644",
4770
- "sha256": "65ce6bcba41342833cd48800e028fd1e3420a4d9d7fb82c1250cfca724baa0ad"
4770
+ "sha256": "74a97d3caf479ffc429b577c4f96cdc33bf45e71f66c0748ac0a4ef22cdb3e60"
4771
4771
  },
4772
4772
  {
4773
4773
  "path": "public-docs/index.html",
@@ -6102,7 +6102,7 @@
6102
6102
  {
6103
6103
  "path": "scripts/gds/ship-land.js",
6104
6104
  "mode": "0000644",
6105
- "sha256": "c3224ab05621b1e1e523b926c7a3d0e09b36c7de3fc6d984ee6c588dfcdd1e38"
6105
+ "sha256": "e091557b4cc29c081cb82f9cd650b4c110f1dede4e72b704893f8397cd7bbaf2"
6106
6106
  },
6107
6107
  {
6108
6108
  "path": "scripts/gds/ship-memory-sync.js",
@@ -6622,7 +6622,7 @@
6622
6622
  {
6623
6623
  "path": "src/module-api.js",
6624
6624
  "mode": "0000644",
6625
- "sha256": "7eca632fc6880711d60fad718a743301e62e99ca990a8a4cf07767726f86f0cf"
6625
+ "sha256": "2509fad9871925f5c6233c9b85e7e31bad63f764892bddd6cc52295442dbc1f9"
6626
6626
  },
6627
6627
  {
6628
6628
  "path": "src/module-loader/catalog.js",
@@ -8977,7 +8977,7 @@
8977
8977
  {
8978
8978
  "path": "tests/ship_publish_error.mjs",
8979
8979
  "mode": "0000644",
8980
- "sha256": "b6ca6ac4802b035c02be382a8aa7f63f5381b02524343d5fe002336c1aadc512"
8980
+ "sha256": "5c0d5961408ac202eb53fe648c4c8b00e5552b231b35f39a39e006b705cb34f1"
8981
8981
  },
8982
8982
  {
8983
8983
  "path": "tests/ship_resume.mjs",
@@ -130,6 +130,23 @@ function scrubCredential(s) {
130
130
  .replace(/(ghs_|ghp_|github_pat_)[A-Za-z0-9_]+/g, '$1***');
131
131
  }
132
132
 
133
+ // How much scrubbed stderr travels with a failure (task 1003222). Sized to hold a
134
+ // real git failure whole — the 'remote:' lines that name the cause arrive LAST, so a
135
+ // short cap deletes precisely the part worth reading. It is a payload bound, nothing
136
+ // more; when it bites, truncateLoudly says so rather than ending mid-word.
137
+ const STDERR_MAX = 8000;
138
+
139
+ // Truncate in a way the reader can SEE. A silent clip is worse than no message: it
140
+ // looks like the complete text, which is how 'error: failed' got mistaken for the
141
+ // whole story. Keeps the TAIL as well as the head, because git's useful line is last.
142
+ function truncateLoudly(text, max = STDERR_MAX) {
143
+ const s = String(text || '');
144
+ if (s.length <= max) return s;
145
+ const head = Math.floor(max * 0.4);
146
+ const tail = max - head;
147
+ return `${s.slice(0, head)}\n\n…[${s.length - max} characters omitted — full text in the server log]…\n\n${s.slice(-tail)}`;
148
+ }
149
+
133
150
  // Default git seam: execFile arg-array, shell:false, stdout captured. On failure
134
151
  // throws GIT_FAILED. We never echo the (possibly credentialed) argv, but DO attach
135
152
  // the credential-scrubbed stderr + the caller's step label (task 1547) so a real
@@ -149,9 +166,30 @@ async function defaultExec(args, opts = {}) {
149
166
  return stdout;
150
167
  } catch (e) {
151
168
  // Identify the failing step by the caller's label, never by echoing argv.
152
- // Surface the scrubbed stderr (truncated) so the cause is loud in the log.
153
- const stderr = scrubCredential(e && (e.stderr || e.message)).trim().slice(0, 500);
154
- throw err('GIT_FAILED', { git_step: step || (args && args[0]) || null, stderr });
169
+ //
170
+ // THE STDERR IS NOT CLIPPED SHORT ANY MORE (task 1003222). It used to be cut at
171
+ // 500 chars here and again at 200 on the way to the client, so a builder saw
172
+ // 'error: failed' — clipped mid-word, naming neither cause nor remedy — while the
173
+ // line that mattered ('remote: fatal error in commit_refs') and the exit code
174
+ // existed only server-side. That cost hours on the blocker-1000117 outage AND
175
+ // produced a WRONG first diagnosis, because the real message was never in the
176
+ // room. git puts the useful line LAST (the 'remote:' lines follow the progress
177
+ // chatter), which is exactly what a head-truncation throws away.
178
+ //
179
+ // The cap is a payload guard, not an editorial decision: generous enough that a
180
+ // real push failure fits whole, and when it does bite the text SAYS SO instead of
181
+ // ending mid-word and reading like the whole message.
182
+ const raw = scrubCredential(e && (e.stderr || e.message)).trim();
183
+ const stderr = truncateLoudly(raw, STDERR_MAX);
184
+ throw err('GIT_FAILED', {
185
+ git_step: step || (args && args[0]) || null,
186
+ stderr,
187
+ stderr_bytes: raw.length,
188
+ // The exit code distinguishes failure SHAPES that read identically in prose —
189
+ // 128 (git said no) vs 1 (a ref was rejected) vs null (killed by the timeout).
190
+ exit_code: (e && typeof e.code === 'number') ? e.code : null,
191
+ timed_out: !!(e && e.killed),
192
+ });
155
193
  }
156
194
  }
157
195
 
@@ -456,7 +494,14 @@ async function openOrFindPullRequest({ branch, title, body }, deps = {}) {
456
494
  });
457
495
  if (!createRes.ok) {
458
496
  const detail = await createRes.text().catch(() => '');
459
- throw err('PR_CREATE_FAILED', { status: createRes.status, detail: detail.slice(0, 300) });
497
+ // Same reasoning as GIT_FAILED's stderr (task 1003222): GitHub's refusal body
498
+ // names the actual rule that refused ('No commits between main and X', a
499
+ // protected-branch rule, a validation error naming the field) and a 300-char
500
+ // clip regularly cut it off. Scrubbed, because a body can echo a request header.
501
+ throw err('PR_CREATE_FAILED', {
502
+ status: createRes.status,
503
+ detail: truncateLoudly(scrubCredential(detail), STDERR_MAX),
504
+ });
460
505
  }
461
506
  const pr = await createRes.json();
462
507
  return { number: pr.number, url: pr.html_url, node_id: pr.node_id };
@@ -1360,6 +1405,8 @@ module.exports = {
1360
1405
  prReferencesTask,
1361
1406
  findTaskPullRequest,
1362
1407
  scrubCredential,
1408
+ truncateLoudly, // task 1003222 — exported so its behaviour is testable, not just its effect
1409
+ STDERR_MAX,
1363
1410
  isAncestorOfDefaultBranch,
1364
1411
  resolveToken,
1365
1412
  getInstallationToken,
@@ -979,7 +979,12 @@ module.exports = function buildTasksRouter() {
979
979
  if (e.code === 'NO_REPO_INFO') return res.fail('no_repo_info', 500);
980
980
  if (e.code === 'PUSH_UNCONFIGURED') return res.fail('push_unconfigured', { status: 503, message: PUSH_UNCONFIGURED_MSG });
981
981
  if (e.code === 'APP_NOT_INSTALLED') return res.fail('push_unconfigured', { status: 503, message: appNotInstalledMessage(e) });
982
- if (e.code === 'PR_CREATE_FAILED') return res.fail('pr_create_failed', 502, { detail: e.detail });
982
+ // The GitHub HTTP status travels WITH the body (task 1003222). 422 ('no commits
983
+ // between'), 403 (a protected-branch rule) and 404 (the App cannot see the repo)
984
+ // are three different remedies, and the body alone does not always separate them.
985
+ if (e.code === 'PR_CREATE_FAILED') {
986
+ return res.fail('pr_create_failed', 502, { detail: e.detail, github_status: e.status ?? null });
987
+ }
983
988
  // task 1547: GIT_FAILED now carries WHICH git step failed + the credential-
984
989
  // scrubbed stderr (github-push.defaultExec). Log both so the next push outage
985
990
  // is a one-line diagnosis, not a multi-hour opaque-`publish_failed` mystery
@@ -990,7 +995,22 @@ module.exports = function buildTasksRouter() {
990
995
  e.git_step ? `step=${e.git_step}` : '',
991
996
  e.stderr ? `stderr=${e.stderr}` : ''
992
997
  );
993
- return res.fail('publish_failed', 500, { ...(e.git_step ? { step: e.git_step } : {}), ...(e.stderr ? { reason: String(e.stderr).slice(0, 200) } : {}) });
998
+ // NO SECOND TRUNCATION (task 1003222). This line used to clip the already-
999
+ // shortened stderr to 200 chars, so the builder got 'error: failed' — cut
1000
+ // mid-word, naming neither cause nor remedy — while the real remote message sat
1001
+ // in a log they could not reach. A publish failure must be diagnosable from
1002
+ // where the builder is STANDING; that is the whole point of the task.
1003
+ //
1004
+ // `reason` keeps its name and position so existing readers (ship.js prints it)
1005
+ // keep working; it is simply no longer amputated. Everything here is already
1006
+ // credential-scrubbed at the throw site by github-push.scrubCredential.
1007
+ return res.fail('publish_failed', 500, {
1008
+ ...(e.git_step ? { step: e.git_step } : {}),
1009
+ ...(e.stderr ? { reason: e.stderr } : {}),
1010
+ ...(e.stderr_bytes != null ? { reason_bytes: e.stderr_bytes } : {}),
1011
+ ...(e.exit_code != null ? { git_exit_code: e.exit_code } : {}),
1012
+ ...(e.timed_out ? { timed_out: true } : {}),
1013
+ });
994
1014
  } finally {
995
1015
  try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
996
1016
  }
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.348",
3
+ "version": "1.19.349",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.348",
9
+ "version": "1.19.349",
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.348",
3
+ "version": "1.19.349",
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",
@@ -89,12 +89,23 @@ function formatPublishFailure(data, status, branch, opts = {}) {
89
89
  const envelopeDetails = (data && data.error && typeof data.error === 'object' && data.error.details) || null;
90
90
  const details = envelopeDetails || data || {};
91
91
  const label = message || code || (data && data.message) || `HTTP ${status}`;
92
- // git stderr is multi-line; this is embedded mid-sentence in one log line.
92
+ // Flattened ONLY for the matchers below and the one-line summary. The readable
93
+ // copy is printed verbatim underneath (task 1003222): the server no longer clips
94
+ // stderr, and git's useful line is the LAST one, so collapsing a 20-line push
95
+ // failure into a single run-on sentence hides the very thing that was un-hidden.
93
96
  const reason = details.reason ? String(details.reason).replace(/\s+/g, ' ').trim() : '';
94
97
  const parts = [];
95
98
  if (details.step) parts.push(`step: ${details.step}`);
96
- if (reason) parts.push(`reason: ${reason}`);
99
+ if (details.git_exit_code != null) parts.push(`git exit ${details.git_exit_code}`);
100
+ if (details.github_status != null) parts.push(`GitHub HTTP ${details.github_status}`);
101
+ if (details.timed_out) parts.push('TIMED OUT');
102
+ // The one-line summary stays short and scannable; the full text follows it.
103
+ if (reason) parts.push(`reason: ${reason.length > 200 ? `${reason.slice(0, 200)}…` : reason}`);
97
104
  let out = parts.length ? `${label} (${parts.join('; ')})` : label;
105
+
106
+ // ORDER MATTERS: summary, then the REMEDY, then the verbatim block last. The
107
+ // remedy is the actionable sentence and must not end up stranded below twenty
108
+ // lines of git chatter where it reads as a footnote to the quote.
98
109
  if (NON_FAST_FORWARD_RE.test(reason)) {
99
110
  out += opts.localHistoryRewritten === true
100
111
  ? ` — your LOCAL history was rewritten (a rebase or amend), so ${branch} is no longer a `
@@ -104,6 +115,17 @@ function formatPublishFailure(data, status, branch, opts = {}) {
104
115
  : ` — the server already moved ${branch}: fetch origin, reset onto origin/${branch}, `
105
116
  + 're-run the generators, and re-ship so the push fast-forwards.';
106
117
  }
118
+
119
+ // The untruncated remote message, verbatim and indented so it reads as a quotation
120
+ // rather than as more ship output. Only when it is multi-line — a single-line
121
+ // stderr is already fully present in the summary above, and repeating it is noise.
122
+ const full = details.reason ? String(details.reason).trimEnd() : '';
123
+ if (full && /\n/.test(full)) {
124
+ out += `\n\n --- what the remote actually said ---\n${full.split('\n').map((l) => ` ${l}`).join('\n')}\n --- end ---`;
125
+ if (details.reason_bytes != null && details.reason_bytes > full.length) {
126
+ out += `\n (${details.reason_bytes} bytes at the server; the middle was omitted here)`;
127
+ }
128
+ }
107
129
  return out;
108
130
  }
109
131
 
package/src/module-api.js CHANGED
@@ -1126,8 +1126,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1126
1126
  // landed since 1.19.346 with no explicit bump. run 32802265337. (task 1002620)
1127
1127
  // 1.19.348 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1128
1128
  // landed since 1.19.347 with no explicit bump. run 32802746363. (task 1002620)
1129
+ // 1.19.349 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1130
+ // landed since 1.19.348 with no explicit bump. run 32803276871. (task 1002620)
1129
1131
  // ---------------------------------------------------------------------------
1130
- const CORE_VERSION = '1.19.348'; // CI auto-patch carrier (ADR 0161); see changelog above
1132
+ const CORE_VERSION = '1.19.349'; // CI auto-patch carrier (ADR 0161); see changelog above
1131
1133
 
1132
1134
  // A namespaced logger so a module's log lines are attributable + consistent.
1133
1135
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -99,7 +99,19 @@ test('a "fetch first" rejection names the branch and the reset-onto-origin remed
99
99
  assert.match(out, /reset/i, 'names the reset');
100
100
  assert.match(out, /generator/i, 'and the regenerate step before re-shipping');
101
101
  assert.match(out, /re-ship|fast-forward/i, 'and why: so the push fast-forwards');
102
- assert.ok(!/\n/.test(out), 'multi-line git stderr is collapsed — this goes mid-sentence into one log line');
102
+ // CONTRACT CHANGED, task 1003222. This used to assert the OPPOSITE — that the
103
+ // whole thing collapsed to one line — which was right while the server clipped
104
+ // stderr to 200 chars, and wrong once it stopped. The summary line is still
105
+ // collapsed and scannable; the verbatim text is quoted BELOW it, because git puts
106
+ // the line that names the cause last and a one-line squash buries it.
107
+ const [summary] = out.split('\n');
108
+ assert.ok(!/\n/.test(summary), 'the SUMMARY is still one scannable line');
109
+ assert.match(summary, /reason:/, 'and still carries a reason inline');
110
+ assert.match(out, /what the remote actually said/, 'the untruncated stderr is quoted below');
111
+ assert.ok(
112
+ out.includes("error: failed to push some refs to 'github.com/org/repo.git'"),
113
+ 'the remote message survives VERBATIM, on its own line — the whole point of the task',
114
+ );
103
115
  });
104
116
 
105
117
  test('the "non-fast-forward" wording triggers the same hint through the envelope', () => {
@@ -158,5 +170,77 @@ test('1003221 a non-NFF failure gains no hint from either flag', () => {
158
170
  }
159
171
  });
160
172
 
173
+
174
+ // --- task 1003222: the remote's own words reach the builder ------------------
175
+ //
176
+ // The failure this closes: a builder saw `reason: error: failed` — clipped
177
+ // mid-word by a 200-char slice on top of an earlier 500-char one — while the line
178
+ // that named the cause (`remote: fatal error in commit_refs`) and the HTTP status
179
+ // existed server-side only. That cost hours AND produced a wrong first diagnosis,
180
+ // because the real message was never in the room.
181
+
182
+ const LONG_STDERR = [
183
+ 'To https://github.com/org/repo.git',
184
+ ' ! [remote rejected] b -> b (pre-receive hook declined)',
185
+ 'error: failed to push some refs',
186
+ 'remote: fatal error in commit_refs', // git's useful line is LAST
187
+ ].join('\n');
188
+
189
+ test('1003222 the LAST line survives — a head-truncation would have eaten it', () => {
190
+ const out = fmt({ error: 'publish_failed', step: 'push', reason: LONG_STDERR });
191
+ assert.ok(
192
+ out.includes('remote: fatal error in commit_refs'),
193
+ 'the cause git printed last must reach the builder',
194
+ );
195
+ });
196
+
197
+ test('1003222 the git exit code is reported — 128 and 1 are different remedies', () => {
198
+ const out = fmt({ error: 'publish_failed', step: 'push', reason: 'boom', git_exit_code: 128 });
199
+ assert.match(out, /git exit 128/);
200
+ });
201
+
202
+ test('1003222 a GitHub HTTP status is reported when the API refused', () => {
203
+ const out = fmt({ error: 'pr_create_failed', github_status: 422, reason: 'No commits between main and b' });
204
+ assert.match(out, /GitHub HTTP 422/, '422 vs 403 vs 404 are three different fixes');
205
+ });
206
+
207
+ test('1003222 a timeout says so rather than looking like a rejection', () => {
208
+ const out = fmt({ error: 'publish_failed', step: 'push', reason: 'killed', timed_out: true });
209
+ assert.match(out, /TIMED OUT/);
210
+ });
211
+
212
+ test('1003222 a single-line stderr is NOT repeated in a quote block', () => {
213
+ // It is already fully present in the summary; quoting it again is noise.
214
+ const out = fmt({ error: 'publish_failed', step: 'push', reason: 'fatal: repository not found' });
215
+ assert.ok(!out.includes('what the remote actually said'), 'no block for a one-liner');
216
+ assert.match(out, /fatal: repository not found/);
217
+ });
218
+
219
+ test('1003222 an omitted middle is DECLARED, never silently clipped', () => {
220
+ const out = fmt({
221
+ error: 'publish_failed', step: 'push', reason: LONG_STDERR, reason_bytes: 99999,
222
+ });
223
+ assert.match(out, /99999 bytes at the server/, 'the reader is told text is missing');
224
+ assert.match(out, /omitted/);
225
+ });
226
+
227
+ test('1003222 the remedy precedes the quote block, not stranded under it', () => {
228
+ // The remedy is the actionable sentence. Below twenty lines of git chatter it
229
+ // reads as a footnote to the quotation and gets missed.
230
+ const out = fmt({ error: 'publish_failed', step: 'push', reason: NON_FF_STDERR });
231
+ const remedy = out.indexOf('reset onto');
232
+ const quote = out.indexOf('what the remote actually said');
233
+ assert.ok(remedy > -1 && quote > -1, 'both present');
234
+ assert.ok(remedy < quote, 'remedy comes first');
235
+ });
236
+
237
+ test('1003222 nothing regressed for a failure carrying no stderr at all', () => {
238
+ const out = fmt({ error: 'publish_failed' });
239
+ assert.match(out, /publish_failed/);
240
+ assert.ok(!out.includes('what the remote actually said'));
241
+ assert.ok(!/undefined|null|\[object/.test(out), 'no placeholder leakage');
242
+ });
243
+
244
+
161
245
  console.log(`ship_publish_error: ${passed} passed, ${failed} failed`);
162
- process.exit(failed === 0 ? 0 : 1);
246
+ process.exit(failed === 0 ? 0 : 1);