@bongos/core 1.19.6 → 1.19.8

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.6",
6
- "core_contract": "1.19.6",
7
- "source_commit": "ec82ce238fe1cf51269e6300752c294fcdff6a8d",
5
+ "core_version": "1.19.8",
6
+ "core_contract": "1.19.8",
7
+ "source_commit": "1dfe885d115a6a4c50fd0298c5087ab67873a781",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-08T20:35:28.636Z",
9
+ "built_at": "2026-08-08T20:41:22.505Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 284,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1202,
14
+ "functional_verbatim": 1203,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1507,
20
- "tree_sha256": "a96a50569de6c81659dff0c6bc5efb47052af5b5efe9a7ad8d96c2205a9e5339",
19
+ "file_count": 1508,
20
+ "tree_sha256": "f1d06886acbcab570fc2f0691d57e71f6a2cd5bccc81e37915420a649e76e85b",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -287,7 +287,7 @@
287
287
  {
288
288
  "path": ".gitattributes",
289
289
  "mode": "0000644",
290
- "sha256": "b1d775aa60944df06acad586311295763956c8cc3a151c0426f83dcc48509b2b"
290
+ "sha256": "106e3a8a087cf1a9dd932513115d16c387cfeebea0e22ed269a672f95efdac1e"
291
291
  },
292
292
  {
293
293
  "path": "CLAUDE.md",
@@ -322,7 +322,7 @@
322
322
  {
323
323
  "path": "README.md",
324
324
  "mode": "0000644",
325
- "sha256": "efd9807a539e2eb478a3a56f5280103cd92e1cf8de97fe995a6fbcb5cb7a4b49"
325
+ "sha256": "f0358ea7bfffc17c7f1e4ead3bb223117787ba6f88c1ae357150e4f6a59e70a8"
326
326
  },
327
327
  {
328
328
  "path": "bin/bongos.js",
@@ -627,7 +627,7 @@
627
627
  {
628
628
  "path": "docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md",
629
629
  "mode": "0000644",
630
- "sha256": "5ca47d7e3e7ea7e4afeb45427c7d53aa3ae83cb4291fadaaca11cf2f85e278cf"
630
+ "sha256": "9feebaa2a6e1133f6eae91e3e8d9bf85fd1e9b3bc276fb862b9097f72fc57621"
631
631
  },
632
632
  {
633
633
  "path": "docs/adr/0044-mediterranean-palette-replacement.md",
@@ -1297,7 +1297,7 @@
1297
1297
  {
1298
1298
  "path": "docs/canonical-permissions.md",
1299
1299
  "mode": "0000644",
1300
- "sha256": "cecb0aba0f0276831422a82e42a8c8473479ad6a59dcb180e38795c91f22d6e2"
1300
+ "sha256": "f57f118f5b863a82895129c70311a7c87e5424e22a793b82c637db2ee7e345a0"
1301
1301
  },
1302
1302
  {
1303
1303
  "path": "docs/design-contract.md",
@@ -3997,12 +3997,12 @@
3997
3997
  {
3998
3998
  "path": "package-lock.json",
3999
3999
  "mode": "0000644",
4000
- "sha256": "2579115bea6d4593a73a28da83fece764ba14bcd519ac1d6fd97ed44cff77fdd"
4000
+ "sha256": "85e3fd43652aab4a9846a8929a542d223fc83549b128babd3633d6a603ffa844"
4001
4001
  },
4002
4002
  {
4003
4003
  "path": "package.json",
4004
4004
  "mode": "0000644",
4005
- "sha256": "04e91b126a9662c0d91bdc0994e5fefe2a49310367b153f151b909de33067ac7"
4005
+ "sha256": "bb2dbdc9f28556ee2a511da1c317206b94b35201334608d8c29182a0ddcfe617"
4006
4006
  },
4007
4007
  {
4008
4008
  "path": "public-docs/index.html",
@@ -4772,7 +4772,7 @@
4772
4772
  {
4773
4773
  "path": "scripts/gds/hooks/pre-push.js",
4774
4774
  "mode": "0000644",
4775
- "sha256": "b1e3df447dfb41e2b826c68f3824c8c037baae7dd357e63b47335da834673116"
4775
+ "sha256": "605b6551ca630222b671b74c08fa1b69d487614359fad327a93c2ac17e8a2021"
4776
4776
  },
4777
4777
  {
4778
4778
  "path": "scripts/gds/http-api-client.js",
@@ -4787,7 +4787,7 @@
4787
4787
  {
4788
4788
  "path": "scripts/gds/install-git-hooks.js",
4789
4789
  "mode": "0000644",
4790
- "sha256": "b5f64bf41bb2b83b2008cef0ce06f4be349060c607aab2825b60f596b418ae05"
4790
+ "sha256": "668d97543e863394527bad2a629037bfc81eea2f8244f53eb5e79fd43f7485c0"
4791
4791
  },
4792
4792
  {
4793
4793
  "path": "scripts/gds/key-card.js",
@@ -5182,7 +5182,7 @@
5182
5182
  {
5183
5183
  "path": "scripts/gds/setup.js",
5184
5184
  "mode": "0000644",
5185
- "sha256": "2665eafc6ba5e37ffa6526f893aa967ddf656f383c4dd04601868e25c71b1939"
5185
+ "sha256": "8e512a07ed885666722a216951dfad7f57fbfd92923bdf5079ce1c6f7b5c0abc"
5186
5186
  },
5187
5187
  {
5188
5188
  "path": "scripts/gds/shell.js",
@@ -5622,7 +5622,7 @@
5622
5622
  {
5623
5623
  "path": "src/module-api.js",
5624
5624
  "mode": "0000644",
5625
- "sha256": "2edb31fa7cb85cba84a757cc80b9e0ef333166b4d21c23884637313ed7eb4e46"
5625
+ "sha256": "9f82f028357ae56a6219c4325bda19f55b339071c090b08c9e01a4c82b9cc9ea"
5626
5626
  },
5627
5627
  {
5628
5628
  "path": "src/module-loader/loader.js",
@@ -7034,6 +7034,11 @@
7034
7034
  "mode": "0000644",
7035
7035
  "sha256": "c9604d5184f2f1243c4bfab594e3f0e2c04060aadc2d726c923e72d38e276577"
7036
7036
  },
7037
+ {
7038
+ "path": "tests/pre_push_hook.mjs",
7039
+ "mode": "0000644",
7040
+ "sha256": "cdd47e5e709590d1f8bb604b9ad98b13857d0efc196c8ab63728f00f62b999b5"
7041
+ },
7037
7042
  {
7038
7043
  "path": "tests/preflight_checks.mjs",
7039
7044
  "mode": "0000644",
package/.gitattributes CHANGED
@@ -17,6 +17,7 @@
17
17
  *.sh text eol=lf
18
18
  *.bash text eol=lf
19
19
  .husky/pre-commit text eol=lf
20
+ .husky/pre-push text eol=lf
20
21
 
21
22
  # Node / Python sources: keep LF everywhere too. Node tolerates CRLF, but LF
22
23
  # keeps diffs clean and avoids surprises when these are sourced or piped.
package/README.md CHANGED
@@ -48,7 +48,7 @@ When it finishes, open Claude Code and run `/builder-start` to see what tasks ar
48
48
 
49
49
  ## Secrets scanning
50
50
 
51
- We keep secrets (`.env` files, API keys, private keys) out of the repo with two layers. Both are **instance topology** — the git hook (`.husky/`) and CI (`.github/`) are provided when you stand up an instance and are not shipped in the portable core; the pattern below is the recommended posture:
51
+ We keep secrets (`.env` files, API keys, private keys) out of the repo with two layers. Both are **instance topology** — the git hooks (`.husky/`) and CI (`.github/`) are excluded from the published core artifact, and a secrets pre-commit hook is an instance addition (nothing provisions one automatically). This repo tracks `.husky/pre-push` — the ADR 0043 permission gate, a separate wall from secrets scanning. The pattern below is the recommended posture:
52
52
 
53
53
  - **Pre-commit hook** (`.husky/pre-commit`) — scans your *staged* changes before each commit and aborts if it finds a secret. On an instance that ships `.husky/`, activate it **once per clone**:
54
54
 
@@ -56,7 +56,7 @@ We keep secrets (`.env` files, API keys, private keys) out of the repo with two
56
56
  node scripts/gds/install-git-hooks.js
57
57
  ```
58
58
 
59
- Prefer that over setting `core.hooksPath` by hand: it no-ops cleanly on a checkout without a `.husky/` (the portable core), whereas pointing `core.hooksPath` at a directory that isn't there leaves git with a dangling hooks path and no gate. `node scripts/gds/doctor.js` reports which case you're in.
59
+ Prefer that over setting `core.hooksPath` by hand: it no-ops cleanly on a checkout without a `.husky/` (e.g. a scaffolded instance that hasn't added hooks), whereas pointing `core.hooksPath` at a directory that isn't there leaves git with a dangling hooks path and no gate. `node scripts/gds/doctor.js` reports which case you're in.
60
60
 
61
61
  It always runs a built-in guard (blocks `.env`, `*.pem`, credential files, obvious key patterns). If [TruffleHog](https://github.com/trufflesecurity/trufflehog) is installed (`brew install trufflehog` on macOS; `scoop install trufflehog` / `choco install trufflehog` or the GitHub release on Windows) it also runs a deep scan; if not, it warns and lets the commit through — CI is the real gate.
62
62
 
@@ -93,6 +93,44 @@ enforcement**, not a redundant second net. Option B (blocker
93
93
  Treat this as the freshness stamp: re-run the probes above before asserting the claim has changed —
94
94
  a plan upgrade or a new ruleset would flip it, and this section is what a future sweep should update.
95
95
 
96
+ ## G-2/G-17 resolution: Layer 1 delivered; the "path bug" refuted (2026-08-08, task 1002608)
97
+
98
+ The 2026-08-06 rank choke-point map (`docs/governance/<redacted>.md` §6) filed
99
+ two findings against Layer 1. Re-verified empirically before acting, per the map's own rule:
100
+
101
+ **G-17 — "the hook could not work even if it were installed" — is REFUTED.** The claim was that
102
+ `pre-push.js:30`'s `path.join(__dirname, '..', '..', '..', 'src', 'bongos', …)` resolves one
103
+ directory *above* the repo root. It does not: `__dirname` is `<root>/scripts/gds/hooks`, and three
104
+ `..` segments land exactly at `<root>` (hooks → gds → scripts → root). Verified live: the require
105
+ resolves and loads (all 15 exports), and the hook fed a `refs/heads/main` line on stdin blocks with
106
+ exit 1. The finding's fail-open claim is also inverted for this failure class: a module-scope
107
+ `require` throw is **not** caught by the `run().catch` crash handler (it wraps `run()` only) — node
108
+ would exit nonzero before any handler installs, and git **aborts** the push. Fail *closed*. The
109
+ runtime crash handler does fail open **by design**, documented in place: Layer 1 is cooperative and
110
+ bypassable (`--no-verify`) regardless, so bricking every push on an unrelated runtime bug buys no
111
+ security — Layer 3 is the net.
112
+
113
+ **G-2 — "no delivery path, and never did" — was real and is now fixed.** `.husky/` had never been
114
+ tracked on any branch, so `install-git-hooks.js` (wired into `/builder-setup` since #863) always
115
+ no-opped with `no-husky-dir`, and `pre-push.js`'s header claimed a `.git/hooks/` install that no
116
+ code performed. The repo now tracks `.husky/pre-push`, a two-line wrapper exec'ing
117
+ `scripts/gds/hooks/pre-push.js` — so the existing activation path (`core.hooksPath=.husky`, set
118
+ per-clone by `/builder-setup` or a standalone `node scripts/gds/install-git-hooks.js`) finally has
119
+ something to activate. This follows the `.github/` precedent from tasks 1002610–1002612: README
120
+ calls both instance topology, and both stay excluded from the **published core artifact**
121
+ (`publish-manifest.js` `PROTECTED_TOPOLOGY_EXCLUDE`, test-asserted) — but this repo is itself the
122
+ live reference instance where the Builder 25 incident happened, so it carries its own walls.
123
+
124
+ **Honest limits, unchanged and new:**
125
+ - Activation is per-clone and best-effort. A clone that never runs `/builder-setup` or
126
+ `install-git-hooks.js` has no Layer 1; whether the operator's own clone is activated cannot be
127
+ settled from the repo.
128
+ - Standalone instances scaffolded from the published core still receive no `.husky/` — README
129
+ §"Secrets scanning" says hooks are "provided when you stand up an instance," but no provisioning
130
+ code materializes them. Captured for the post-wave bug phase (idea inbox) rather than fixed here.
131
+ - The layer table above stands: Layer 1 is still not a trust boundary; the post-push main audit —
132
+ since task 1002612 a CI job on every push to `main` — remains the authoritative layer.
133
+
96
134
  ## Notes
97
135
 
98
136
  We did **not** take immediate action against Builder 25 — Lars was overseeing the session and the change itself was a legitimate bug fix. The point of this ADR is the *mechanism*, not the person: the system must make the bypass hard and visible regardless of intent.
@@ -260,7 +260,9 @@ for the probes. `main-audit.js` is therefore still the *authoritative* layer, no
260
260
  rank/authz machinery, the ship/grade/deploy pipeline, DB `migrations/`, `infra/`, and the
261
261
  trust-boundary docs. The protected set is the glob list in
262
262
  [`src/bongos/permission-path-check.js`](../src/bongos/permission-path-check.js) (source of truth).
263
- - **Nobody pushes `main` directly.** The pre-push hook (`.husky/pre-push`) blocks it; the graded
263
+ - **Nobody pushes `main` directly.** The pre-push hook (`.husky/pre-push`, tracked in this repo and
264
+ activated per-clone by `/builder-setup` via `install-git-hooks.js` — inert on a clone that never
265
+ activated it; ADR 0043 §"G-2/G-17 resolution") blocks it; the graded
264
266
  pipeline pushes only via `scripts/gds/push-main.js` (which carries the `OTB_ALLOW_MAIN_PUSH`
265
267
  override). A raw push is caught and reverted by the **post-push main audit**
266
268
  (`scripts/gds/main-audit.js`) — nightly and as the `~/deploy.sh` deploy gate — which is the
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.6",
3
+ "version": "1.19.8",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.6",
9
+ "version": "1.19.8",
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.6",
3
+ "version": "1.19.8",
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",
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  // scripts/gds/hooks/pre-push.js — client-side push gate (SEC #863).
3
3
  //
4
- // Installed as .git/hooks/pre-push by scripts/gds/install-git-hooks.js (run from
5
- // /builder-setup). This is LAYER 1 of the #863 fix — fast, cooperative, blocks
4
+ // Invoked via the tracked .husky/pre-push wrapper. Inert until a clone runs
5
+ // scripts/gds/install-git-hooks.js (from /builder-setup), which points
6
+ // core.hooksPath at .husky/ — activation is per-clone (task 1002608; ADR 0043
7
+ // §"G-2/G-17 resolution"). The published core artifact excludes .husky/
8
+ // (publish-manifest.js PROTECTED_TOPOLOGY_EXCLUDE) — instance topology.
9
+ // This is LAYER 1 of the #863 fix — fast, cooperative, blocks
6
10
  // the honest builder before a bad push ever leaves the machine. It is NOT a
7
11
  // trust boundary on its own (a builder can `git push --no-verify` or delete the
8
12
  // hook); the authoritative net is the post-push main audit + the deploy-side
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  // scripts/gds/install-git-hooks.js — activate the project's git hooks (SEC #863).
3
3
  //
4
- // The Off the Boats hooks are TRACKED, committed files under .husky/ (the same
5
- // pattern as .husky/pre-commit, the secrets guard). Git only runs them when the
6
- // clone has core.hooksPath pointed at .husky. The pre-commit hook documents
7
- // "Activate once per clone: git config core.hooksPath .husky" — easy to forget,
8
- // which means a builder could run with NO secret-scan AND NO push gate.
4
+ // The hooks are TRACKED, committed files under .husky/ — this repo ships
5
+ // .husky/pre-push (the ADR 0043 Layer 1 permission gate, task 1002608); an
6
+ // instance may add more (e.g. a pre-commit secrets guard). Git only runs them
7
+ // when the clone has core.hooksPath pointed at .husky — easy to forget, which
8
+ // means a builder could run with NO push gate.
9
9
  //
10
10
  // This script makes activation automatic (called by /builder-setup). It does
11
11
  // NOT write any hook file — the hooks ship via git. It only ensures
12
- // core.hooksPath is set so .husky/pre-commit AND .husky/pre-push fire.
12
+ // core.hooksPath is set so whatever hooks .husky/ tracks actually fire.
13
13
  //
14
14
  // Idempotent and cross-platform. Runnable standalone:
15
15
  // node scripts/gds/install-git-hooks.js
@@ -68,6 +68,8 @@ function activate() {
68
68
  // clones pick it up on any re-run of this script.
69
69
  const mergeDriver = registerMergeDriver(root);
70
70
 
71
+ const hooks = fs.readdirSync(huskyDir).filter((f) => !f.startsWith('.') && !f.startsWith('_'));
72
+
71
73
  let current = '';
72
74
  try { current = git('config --get core.hooksPath'); } catch (_) { current = ''; }
73
75
 
@@ -77,13 +79,13 @@ function activate() {
77
79
  const points = configuredHooksDir();
78
80
  const alreadyHusky = current && points && path.basename(points) === '.husky';
79
81
  if (alreadyHusky) {
80
- return { status: 'already-active', hooksPath: current, dir: points, mergeDriver };
82
+ return { status: 'already-active', hooksPath: current, dir: points, mergeDriver, hooks };
81
83
  }
82
84
 
83
85
  // Not set (or set elsewhere): activate the project hooks. Relative '.husky'
84
86
  // is husky-standard and resolves per-worktree.
85
87
  git('config core.hooksPath .husky');
86
- return { status: 'activated', hooksPath: '.husky', dir: huskyDir, mergeDriver };
88
+ return { status: 'activated', hooksPath: '.husky', dir: huskyDir, mergeDriver, hooks };
87
89
  }
88
90
 
89
91
  if (require.main === module) {
@@ -93,10 +95,11 @@ if (require.main === module) {
93
95
  console.error(`⚠ ${r.message}`);
94
96
  process.exit(0);
95
97
  }
98
+ const live = (r.hooks && r.hooks.length) ? r.hooks.join(', ') : 'none tracked';
96
99
  if (r.status === 'already-active') {
97
- console.log(`✓ git hooks already active (core.hooksPath=${r.hooksPath}); pre-commit + pre-push gates live`);
100
+ console.log(`✓ git hooks already active (core.hooksPath=${r.hooksPath}); live hooks: ${live}`);
98
101
  } else {
99
- console.log(`✓ git hooks activated (core.hooksPath=.husky); pre-commit secrets guard + pre-push permission gate now run`);
102
+ console.log(`✓ git hooks activated (core.hooksPath=.husky); live hooks: ${live}`);
100
103
  }
101
104
  if (r.mergeDriver && r.mergeDriver.ok) {
102
105
  console.log(`✓ merge driver '${MERGE_DRIVER_NAME}' registered; generated-file conflicts (docs/repo-map.md, nested CLAUDE.md) auto-regenerate on merge`);
@@ -515,14 +515,16 @@ async function promptOnboardingConsent() {
515
515
  console.log(`Total credits: ${builder.total_credits}`);
516
516
  console.log(`Session saved to ${SESSION_PATH}`);
517
517
  // SEC #863: activate the project git hooks (core.hooksPath → .husky) so
518
- // the pre-commit secrets guard AND the pre-push permission gate run. Best
519
- // effort — an activation hiccup must never fail an otherwise-good setup;
520
- // the post-push main audit is the fail-closed net regardless.
518
+ // the tracked hooks run — here that's the pre-push permission gate
519
+ // (task 1002608); an instance may track more. Best effort — an activation
520
+ // hiccup must never fail an otherwise-good setup; the post-push main
521
+ // audit is the fail-closed net regardless.
521
522
  try {
522
523
  const { activate } = require('./install-git-hooks');
523
524
  const r = activate();
524
- if (r.status === 'activated') console.log('Git hooks activated (pre-commit secrets guard + pre-push permission gate)');
525
- else if (r.status === 'already-active') console.log('Git hooks already active (pre-commit + pre-push gates)');
525
+ const live = (r.hooks && r.hooks.length) ? r.hooks.join(', ') : 'none tracked';
526
+ if (r.status === 'activated') console.log(`Git hooks activated (live hooks: ${live})`);
527
+ else if (r.status === 'already-active') console.log(`Git hooks already active (live hooks: ${live})`);
526
528
  else console.log(`Note: ${r.message}`);
527
529
  } catch (e) {
528
530
  console.log(`Note: could not activate git hooks (${e.message}); run: git config core.hooksPath .husky`);
package/src/module-api.js CHANGED
@@ -439,8 +439,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
439
439
  // landed since 1.19.4 with no explicit bump. run 31276103393. (task 1002620)
440
440
  // 1.19.6 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
441
441
  // landed since 1.19.5 with no explicit bump. run 31277243929. (task 1002620)
442
+ // 1.19.7 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
443
+ // landed since 1.19.6 with no explicit bump. run 31277387539. (task 1002620)
444
+ // 1.19.8 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
445
+ // landed since 1.19.7 with no explicit bump. run 31277471863. (task 1002620)
442
446
  // ---------------------------------------------------------------------------
443
- const CORE_VERSION = '1.19.6'; // CI auto-patch carrier (ADR 0161); see changelog above
447
+ const CORE_VERSION = '1.19.8'; // CI auto-patch carrier (ADR 0161); see changelog above
444
448
 
445
449
  // A namespaced logger so a module's log lines are attributable + consistent.
446
450
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,77 @@
1
+ // tests/pre_push_hook.mjs — ADR 0043 Layer 1 delivery + behavior (task 1002608, G-2/G-17).
2
+ //
3
+ // Pins three facts the 2026-08-06 choke-point map litigated:
4
+ // 1. G-17 refutation: pre-push.js's dependency path resolves INSIDE the repo
5
+ // (three `..` from scripts/gds/hooks/ is the root, not one above it).
6
+ // 2. The hook actually blocks a direct push to refs/heads/main (exit 1),
7
+ // and allows a branch deletion (exit 0) — both offline-deterministic.
8
+ // 3. G-2 fix: the tracked .husky/pre-push wrapper exists, execs the real
9
+ // gate, and is LF + executable in the git index (CRLF or mode 100644
10
+ // would silently kill the hook on Unix clones).
11
+ //
12
+ // DB-free, no network: the main-push block fires before any API call.
13
+ // Run: node --test tests/pre_push_hook.mjs
14
+
15
+ import assert from 'node:assert/strict';
16
+ import { test } from 'node:test';
17
+ import { createRequire } from 'node:module';
18
+ import { spawnSync } from 'node:child_process';
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+
23
+ const require = createRequire(import.meta.url);
24
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
+ const HOOK = path.join(ROOT, 'scripts', 'gds', 'hooks', 'pre-push.js');
26
+ const WRAPPER = path.join(ROOT, '.husky', 'pre-push');
27
+ const SHA = 'a'.repeat(40);
28
+ const ZEROS = '0'.repeat(40);
29
+
30
+ function runHook(stdinLine) {
31
+ return spawnSync(process.execPath, [HOOK, 'origin', 'https://example.invalid/repo.git'], {
32
+ cwd: ROOT,
33
+ input: stdinLine,
34
+ encoding: 'utf8',
35
+ env: { ...process.env, OTB_ALLOW_MAIN_PUSH: '' },
36
+ });
37
+ }
38
+
39
+ test('G-17 refuted: the line-30 dependency path resolves inside the repo and loads', () => {
40
+ const hooksDir = path.dirname(HOOK);
41
+ const resolved = path.join(hooksDir, '..', '..', '..', 'src', 'bongos', 'permission-path-check.js');
42
+ assert.equal(path.resolve(resolved), path.join(ROOT, 'src', 'bongos', 'permission-path-check.js'));
43
+ assert.ok(fs.existsSync(resolved), 'permission-path-check.js must exist where pre-push.js requires it');
44
+ const ppc = require(resolved);
45
+ assert.equal(typeof ppc.checkPermissionPaths, 'function');
46
+ });
47
+
48
+ test('hook blocks a direct push to refs/heads/main with exit 1', () => {
49
+ const r = runHook(`refs/heads/main ${SHA} refs/heads/main ${SHA}\n`);
50
+ assert.equal(r.status, 1, `expected exit 1, got ${r.status}; stderr: ${r.stderr}`);
51
+ assert.match(r.stderr, /push blocked/i);
52
+ assert.match(r.stderr, /Direct pushes to `main`/);
53
+ });
54
+
55
+ test('hook allows a branch deletion (all-zero local sha) with exit 0', () => {
56
+ const r = runHook(`refs/heads/some-branch ${ZEROS} refs/heads/some-branch ${SHA}\n`);
57
+ assert.equal(r.status, 0, `expected exit 0, got ${r.status}; stderr: ${r.stderr}`);
58
+ });
59
+
60
+ test('requiring the hook as a module neither reads stdin nor exits', () => {
61
+ const mod = require(HOOK);
62
+ assert.equal(typeof mod.run, 'function');
63
+ });
64
+
65
+ test('G-2 fixed: tracked .husky/pre-push wrapper execs the real gate', () => {
66
+ assert.ok(fs.existsSync(WRAPPER), '.husky/pre-push must be tracked (ADR 0043 Layer 1 delivery)');
67
+ const body = fs.readFileSync(WRAPPER, 'utf8');
68
+ assert.ok(body.startsWith('#!/bin/sh'), 'wrapper must start with a sh shebang');
69
+ assert.match(body, /scripts\/gds\/hooks\/pre-push\.js/, 'wrapper must invoke the real gate');
70
+ assert.ok(!body.includes('\r'), 'wrapper must be LF-only (CRLF breaks the shebang on Unix)');
71
+ });
72
+
73
+ test('wrapper is executable (mode 100755) in the git index', () => {
74
+ const r = spawnSync('git', ['ls-files', '-s', '--', '.husky/pre-push'], { cwd: ROOT, encoding: 'utf8' });
75
+ assert.equal(r.status, 0, `git ls-files failed: ${r.stderr}`);
76
+ assert.match(r.stdout, /^100755 /, `expected index mode 100755, got: ${r.stdout.trim() || '(untracked)'}`);
77
+ });