@bongos/core 1.19.11 → 1.19.13

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.11",
6
- "core_contract": "1.19.11",
7
- "source_commit": "fbce8088d2da2e7fa918dbde45c56e99b515e758",
5
+ "core_version": "1.19.13",
6
+ "core_contract": "1.19.13",
7
+ "source_commit": "9871ae1c84d380c804f27a2a8f2c8b31a1c0eae2",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-08T20:47:47.820Z",
9
+ "built_at": "2026-08-08T20:54:03.577Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 284,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 1508,
20
- "tree_sha256": "f625f5bb236d38733faaf7e6c684f729794116ad9eaa1668ab5574c7b8f643f1",
20
+ "tree_sha256": "ee0b74c675d9eae8a9eacfe1e32c2c3b89f6b30cde100b786e5c4dd04f859ede",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -3997,12 +3997,12 @@
3997
3997
  {
3998
3998
  "path": "package-lock.json",
3999
3999
  "mode": "0000644",
4000
- "sha256": "4f297848afb8580eeefc32077b5a46385dd1e5746c4142498286762845ee6ec8"
4000
+ "sha256": "e651e8a285a3591a06a4562fef0fe2b92a412da95e879f12a891d743358f4dd9"
4001
4001
  },
4002
4002
  {
4003
4003
  "path": "package.json",
4004
4004
  "mode": "0000644",
4005
- "sha256": "c4d0f571c09c5a7cdd3484e326cfa763fbeb6b5b27f369a1d83bd766ffd7fe78"
4005
+ "sha256": "4649829e05fa95e32332b71cdc37037d194f4f15b7c47af6618eb1f00dd2f785"
4006
4006
  },
4007
4007
  {
4008
4008
  "path": "public-docs/index.html",
@@ -5192,7 +5192,7 @@
5192
5192
  {
5193
5193
  "path": "scripts/gds/ship.js",
5194
5194
  "mode": "0000644",
5195
- "sha256": "fc83c3ee63ce06c603eff9a4423f21d3b915ea7eae27e68ba953a45c8f14eda6"
5195
+ "sha256": "bea8a5d8698abf30064c005076c069d470203c86a37a1ff7a3aebc4a5ded19a6"
5196
5196
  },
5197
5197
  {
5198
5198
  "path": "scripts/gds/smoke-batch-claim.sh",
@@ -5622,7 +5622,7 @@
5622
5622
  {
5623
5623
  "path": "src/module-api.js",
5624
5624
  "mode": "0000644",
5625
- "sha256": "6039c907e9885991e682e1d0db84c308229153599e2331a4bea08a97d81839ba"
5625
+ "sha256": "138d3c390e59579f2f8b68f2bed364df776af9e1374c62ce6d12dc9a0fe8a43d"
5626
5626
  },
5627
5627
  {
5628
5628
  "path": "src/module-loader/loader.js",
@@ -7367,7 +7367,7 @@
7367
7367
  {
7368
7368
  "path": "tests/ship_ci_deploy.mjs",
7369
7369
  "mode": "0000644",
7370
- "sha256": "02346e000b0b4212a27dce1d2638a52b2e80509eea335110a148886f18ee5b81"
7370
+ "sha256": "de59e8ed0d1a0e30b52994fe1bf3e1864706613a1329d967f74eeae277c1948e"
7371
7371
  },
7372
7372
  {
7373
7373
  "path": "tests/ship_preflight.mjs",
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.11",
3
+ "version": "1.19.13",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.11",
9
+ "version": "1.19.13",
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.11",
3
+ "version": "1.19.13",
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",
@@ -831,27 +831,58 @@ function parseDeployMode(envVal, configRaw) {
831
831
  return 'laptop';
832
832
  }
833
833
 
834
- // effectiveDeployMode — credential-aware adjustment of the parsed mode (pure;
835
- // exported for the regression test). The 'laptop' land path pushes main + SSHes
836
- // the droplet FROM THIS MACHINE, so it requires a local GitHub push credential.
837
- // A machine without one — a dev box, or a fresh/stale local clone that can't even
838
- // `git pull` (token-less origin, ADR 0053) — physically cannot run it; it must
839
- // ship via the 'ci' server-mediated publish path instead (pushVia()==='server',
840
- // ADR 0055), which lets the GDS server push with its own credential. So when the
841
- // parsed mode is 'laptop' but the machine can't push locally AND the mode was NOT
842
- // pinned by an explicit OTB_DEPLOY_MODE, fall back to 'ci' rather than strand
843
- // every ship at merge. This is the scootermack 2026-06-22 case: a checkout left
844
- // at the pre-cutover 'laptop' default that can't pull forward to 'ci', so it kept
845
- // attempting a push it had no credential for. An explicit env override always
846
- // wins (tests/staging may force 'laptop'); a machine WITH local push creds (a
847
- // real authed laptop) is byte-for-byte unchanged. Mirrors pushVia()'s existing
848
- // credential-awareness — the asymmetry (pushVia smart, resolveDeployMode blind)
849
- // WAS the bug. (task 1449)
850
- function effectiveDeployMode(mode, { canPushLocally, envForced } = {}) {
834
+ // effectiveDeployMode — credential- and protection-aware adjustment of the parsed
835
+ // mode (pure; exported for the regression test). The 'laptop' land path pushes
836
+ // main + SSHes the droplet FROM THIS MACHINE, so it requires a local GitHub push
837
+ // credential. A machine without one — a dev box, or a fresh/stale local clone that
838
+ // can't even `git pull` (token-less origin, ADR 0053) — physically cannot run it;
839
+ // it must ship via the 'ci' server-mediated publish path instead
840
+ // (pushVia()==='server', ADR 0055), which lets the GDS server push with its own
841
+ // credential. So when the parsed mode is 'laptop' but the machine can't push
842
+ // locally AND the mode was NOT pinned by an explicit OTB_DEPLOY_MODE, fall back to
843
+ // 'ci' rather than strand every ship at merge. This is the scootermack 2026-06-22
844
+ // case: a checkout left at the pre-cutover 'laptop' default that can't pull
845
+ // forward to 'ci', so it kept attempting a push it had no credential for. An
846
+ // explicit env override always wins (tests/staging may force 'laptop'). Mirrors
847
+ // pushVia()'s existing credential-awareness — the asymmetry (pushVia smart,
848
+ // resolveDeployMode blind) WAS the bug. (task 1449)
849
+ //
850
+ // Second input, same shape (task 1002629): a machine WITH push credentials but a
851
+ // branch-PROTECTED main also cannot run the laptop land — GitHub refuses the
852
+ // direct `git push origin main` no matter whose credential it is. When task
853
+ // 1002619 armed protection (required `unit` check), every ship resolved 'laptop'
854
+ // and pushed into the wall, stranding graded-PASS tasks at confirmed (tasks
855
+ // 1002608/1002609; learning 1000177). mainProtected must be STRICTLY true to
856
+ // trigger — null/undefined means the probe couldn't answer (no gh, offline, no
857
+ // permission) and keeps today's behavior rather than rerouting on a guess.
858
+ function effectiveDeployMode(mode, { canPushLocally, envForced, mainProtected } = {}) {
851
859
  if (mode === 'laptop' && !envForced && !canPushLocally) return 'ci';
860
+ if (mode === 'laptop' && !envForced && mainProtected === true) return 'ci';
852
861
  return mode;
853
862
  }
854
863
 
864
+ // mainIsProtected — true/false when GitHub answers, null when it can't. The
865
+ // `gh api` resolves {owner}/{repo} from the checkout's origin; any failure
866
+ // (gh missing, offline, non-200) is null = unknown, which effectiveDeployMode
867
+ // treats as "no change". Same spawn discipline as ghAvailableAndAuthed().
868
+ function mainIsProtected() {
869
+ try {
870
+ const r = spawnSync('gh', ['api', 'repos/{owner}/{repo}/branches/main', '--jq', '.protected'], {
871
+ encoding: 'utf8',
872
+ shell: process.platform === 'win32',
873
+ windowsHide: true,
874
+ timeout: 15000,
875
+ });
876
+ if (!r || r.error || r.status !== 0) return null;
877
+ const out = String(r.stdout || '').trim();
878
+ if (out === 'true') return true;
879
+ if (out === 'false') return false;
880
+ return null;
881
+ } catch (_) {
882
+ return null;
883
+ }
884
+ }
885
+
855
886
  function resolveDeployMode() {
856
887
  const configRaw = readDeployConfigRaw();
857
888
  const mode = parseDeployMode(process.env.OTB_DEPLOY_MODE, configRaw);
@@ -860,9 +891,17 @@ function resolveDeployMode() {
860
891
  );
861
892
  // Probe `gh` ONLY when it can change the answer (resolved 'laptop', no env
862
893
  // override) — the happy 'ci' path and an env-pinned mode spawn no extra gh,
863
- // keeping the documented spawn discipline (the line-1019 note).
894
+ // keeping the documented spawn discipline (the line-1019 note). The protection
895
+ // probe runs only when the credential check kept us on 'laptop' (a
896
+ // credential-less machine already falls back without it).
864
897
  if (mode === 'laptop' && !envForced) {
865
- return effectiveDeployMode(mode, { canPushLocally: ghAvailableAndAuthed(), envForced });
898
+ const canPushLocally = ghAvailableAndAuthed();
899
+ const mainProtected = canPushLocally ? mainIsProtected() : null;
900
+ const effective = effectiveDeployMode(mode, { canPushLocally, envForced, mainProtected });
901
+ if (effective === 'ci' && mainProtected === true) {
902
+ console.log(' deploy mode: main is branch-protected (direct pushes refused) — landing via the ci PR path');
903
+ }
904
+ return effective;
866
905
  }
867
906
  return mode;
868
907
  }
@@ -2486,6 +2525,13 @@ async function autoMerge(taskId, valueSummary, { dbOnly = false } = {}) {
2486
2525
  // cross-platform reason as the other node spawns here (#660).
2487
2526
  if (!shellOk(`"${process.execPath}" ${JSON.stringify(path.join(__dirname, 'push-main.js'))}`, { cwd: REPO_ROOT })) {
2488
2527
  console.log(' auto-merge: push failed; task stays at confirmed');
2528
+ // task 1002629: a branch-protected main refuses EVERY direct push, so
2529
+ // retrying here can never work — resolveDeployMode normally reroutes to
2530
+ // the ci PR path before this point; reaching this with protection on
2531
+ // means an env-forced 'laptop' or a failed protection probe.
2532
+ if (mainIsProtected() === true) {
2533
+ return landBailed('main is branch-protected — the direct push can never land; unset OTB_DEPLOY_MODE (the ci PR path lands it)', `node scripts/gds/ship.js ${taskId}`);
2534
+ }
2489
2535
  return landBailed('could not push the merged main to origin', '/merge-mode');
2490
2536
  }
2491
2537
 
package/src/module-api.js CHANGED
@@ -449,8 +449,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
449
449
  // landed since 1.19.9 with no explicit bump. run 31277644352. (task 1002620)
450
450
  // 1.19.11 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
451
451
  // landed since 1.19.10 with no explicit bump. run 31277735279. (task 1002620)
452
+ // 1.19.12 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
453
+ // landed since 1.19.11 with no explicit bump. run 31277905072. (task 1002620)
454
+ // 1.19.13 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
455
+ // landed since 1.19.12 with no explicit bump. run 31277989346. (task 1002620)
452
456
  // ---------------------------------------------------------------------------
453
- const CORE_VERSION = '1.19.11'; // CI auto-patch carrier (ADR 0161); see changelog above
457
+ const CORE_VERSION = '1.19.13'; // CI auto-patch carrier (ADR 0161); see changelog above
454
458
 
455
459
  // A namespaced logger so a module's log lines are attributable + consistent.
456
460
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -544,6 +544,42 @@ test('effectiveDeployMode: tolerates a missing options object (defaults to no fa
544
544
  assert.equal(effectiveDeployMode('laptop'), 'ci'); // canPushLocally undefined ⇒ falsy ⇒ fall back
545
545
  });
546
546
 
547
+ // ---- 10b. effectiveDeployMode: a PROTECTED main never strands a push-capable laptop (task 1002629) ----
548
+ //
549
+ // The wave-4 strand: task 1002619 armed branch protection on main (required
550
+ // `unit` check), so the laptop land's direct `git push origin main` is refused
551
+ // by GitHub for EVERY ship. A machine WITH push credentials resolved 'laptop'
552
+ // and pushed straight into the wall — tasks 1002608/1002609 graded PASS, then
553
+ // stranded at confirmed (learning 1000177). Credential-awareness (task 1449,
554
+ // section 10) cannot see this: the credential is fine, the REF is protected.
555
+ // Same fix shape, second input. mainProtected must be STRICTLY true to trigger
556
+ // — unknown (null/undefined: no gh, offline, no permission) keeps today's
557
+ // behavior rather than silently rerouting on a failed probe.
558
+
559
+ test('effectiveDeployMode: laptop + CAN push + main PROTECTED + not env-forced → ci (the fix)', () => {
560
+ assert.equal(effectiveDeployMode('laptop', { canPushLocally: true, envForced: false, mainProtected: true }), 'ci');
561
+ });
562
+
563
+ test('effectiveDeployMode: laptop + main protected but ENV-FORCED → laptop (explicit override wins)', () => {
564
+ assert.equal(effectiveDeployMode('laptop', { canPushLocally: true, envForced: true, mainProtected: true }), 'laptop');
565
+ });
566
+
567
+ test('effectiveDeployMode: laptop + protection UNKNOWN → laptop (a failed probe must not reroute)', () => {
568
+ assert.equal(effectiveDeployMode('laptop', { canPushLocally: true, envForced: false, mainProtected: null }), 'laptop');
569
+ assert.equal(effectiveDeployMode('laptop', { canPushLocally: true, envForced: false, mainProtected: undefined }), 'laptop');
570
+ assert.equal(effectiveDeployMode('laptop', { canPushLocally: true, envForced: false }), 'laptop');
571
+ });
572
+
573
+ test('effectiveDeployMode: ci stays ci for every protection value', () => {
574
+ assert.equal(effectiveDeployMode('ci', { canPushLocally: true, envForced: false, mainProtected: true }), 'ci');
575
+ assert.equal(effectiveDeployMode('ci', { canPushLocally: true, envForced: true, mainProtected: false }), 'ci');
576
+ assert.equal(effectiveDeployMode('ci', { canPushLocally: false, envForced: false, mainProtected: null }), 'ci');
577
+ });
578
+
579
+ test('effectiveDeployMode: both fallbacks compose — credential rule fires even when protection is unknown', () => {
580
+ assert.equal(effectiveDeployMode('laptop', { canPushLocally: false, envForced: false, mainProtected: null }), 'ci');
581
+ });
582
+
547
583
  // ---- task 1002606: the success-pending banner carries the strand-detection story ----
548
584
  // Finding P-3: the banner ended at "This is NOT a failure", so when a land DID die
549
585
  // (hand-written conflict, dead PR, red check) the builder's last signal said all was