repo-contract 0.1.1 → 0.3.0

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.
Files changed (102) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +56 -1
  3. package/dist/index.cjs +60 -26
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.js +60 -23
  6. package/dist/index.js.map +1 -1
  7. package/package.json +6 -7
  8. package/src/config/validate-config.ts +63 -8
  9. package/src/execution/process-tree.ts +22 -6
  10. package/src/execution/run-checks.ts +4 -2
  11. package/src/execution/spawn-check.ts +63 -23
  12. package/src/index.ts +2 -0
  13. package/src/run-repo-contract.ts +12 -1
  14. package/src/types.ts +86 -3
  15. package/dist/.dts/config/define-repo-contract.d.ts +0 -36
  16. package/dist/.dts/config/define-repo-contract.d.ts.map +0 -1
  17. package/dist/.dts/config/tokenize-command.d.ts +0 -33
  18. package/dist/.dts/config/tokenize-command.d.ts.map +0 -1
  19. package/dist/.dts/config/validate-config.d.ts +0 -32
  20. package/dist/.dts/config/validate-config.d.ts.map +0 -1
  21. package/dist/.dts/errors.d.ts +0 -155
  22. package/dist/.dts/errors.d.ts.map +0 -1
  23. package/dist/.dts/evidence/build-evidence.d.ts +0 -26
  24. package/dist/.dts/evidence/build-evidence.d.ts.map +0 -1
  25. package/dist/.dts/execution/abort-signals.d.ts +0 -29
  26. package/dist/.dts/execution/abort-signals.d.ts.map +0 -1
  27. package/dist/.dts/execution/concurrency-pool.d.ts +0 -14
  28. package/dist/.dts/execution/concurrency-pool.d.ts.map +0 -1
  29. package/dist/.dts/execution/dependency-scheduler.d.ts +0 -30
  30. package/dist/.dts/execution/dependency-scheduler.d.ts.map +0 -1
  31. package/dist/.dts/execution/process-tree.d.ts +0 -48
  32. package/dist/.dts/execution/process-tree.d.ts.map +0 -1
  33. package/dist/.dts/execution/run-checks.d.ts +0 -29
  34. package/dist/.dts/execution/run-checks.d.ts.map +0 -1
  35. package/dist/.dts/execution/spawn-check.d.ts +0 -30
  36. package/dist/.dts/execution/spawn-check.d.ts.map +0 -1
  37. package/dist/.dts/index.d.ts +0 -13
  38. package/dist/.dts/index.d.ts.map +0 -1
  39. package/dist/.dts/parsing/parse-json.d.ts +0 -8
  40. package/dist/.dts/parsing/parse-json.d.ts.map +0 -1
  41. package/dist/.dts/parsing/parse-output.d.ts +0 -10
  42. package/dist/.dts/parsing/parse-output.d.ts.map +0 -1
  43. package/dist/.dts/parsing/parse-text.d.ts +0 -8
  44. package/dist/.dts/parsing/parse-text.d.ts.map +0 -1
  45. package/dist/.dts/parsing/parse-yaml.d.ts +0 -21
  46. package/dist/.dts/parsing/parse-yaml.d.ts.map +0 -1
  47. package/dist/.dts/policy/run-policies.d.ts +0 -38
  48. package/dist/.dts/policy/run-policies.d.ts.map +0 -1
  49. package/dist/.dts/presets/arethetypeswrong.d.ts +0 -39
  50. package/dist/.dts/presets/arethetypeswrong.d.ts.map +0 -1
  51. package/dist/.dts/presets/broken-links.d.ts +0 -16
  52. package/dist/.dts/presets/broken-links.d.ts.map +0 -1
  53. package/dist/.dts/presets/commitlint.d.ts +0 -22
  54. package/dist/.dts/presets/commitlint.d.ts.map +0 -1
  55. package/dist/.dts/presets/dead-code.d.ts +0 -23
  56. package/dist/.dts/presets/dead-code.d.ts.map +0 -1
  57. package/dist/.dts/presets/duplication.d.ts +0 -14
  58. package/dist/.dts/presets/duplication.d.ts.map +0 -1
  59. package/dist/.dts/presets/e2e.d.ts +0 -4
  60. package/dist/.dts/presets/e2e.d.ts.map +0 -1
  61. package/dist/.dts/presets/format.d.ts +0 -4
  62. package/dist/.dts/presets/format.d.ts.map +0 -1
  63. package/dist/.dts/presets/index.d.ts +0 -31
  64. package/dist/.dts/presets/index.d.ts.map +0 -1
  65. package/dist/.dts/presets/license.d.ts +0 -4
  66. package/dist/.dts/presets/license.d.ts.map +0 -1
  67. package/dist/.dts/presets/lint.d.ts +0 -20
  68. package/dist/.dts/presets/lint.d.ts.map +0 -1
  69. package/dist/.dts/presets/markdownlint.d.ts +0 -23
  70. package/dist/.dts/presets/markdownlint.d.ts.map +0 -1
  71. package/dist/.dts/presets/publint.d.ts +0 -13
  72. package/dist/.dts/presets/publint.d.ts.map +0 -1
  73. package/dist/.dts/presets/security-deps.d.ts +0 -4
  74. package/dist/.dts/presets/security-deps.d.ts.map +0 -1
  75. package/dist/.dts/presets/security-secrets.d.ts +0 -4
  76. package/dist/.dts/presets/security-secrets.d.ts.map +0 -1
  77. package/dist/.dts/presets/shared/error-warning-pass-policy.d.ts +0 -13
  78. package/dist/.dts/presets/shared/error-warning-pass-policy.d.ts.map +0 -1
  79. package/dist/.dts/presets/shared/exit-code-fail-rationale.d.ts +0 -23
  80. package/dist/.dts/presets/shared/exit-code-fail-rationale.d.ts.map +0 -1
  81. package/dist/.dts/presets/shared/missing-dependency.d.ts +0 -19
  82. package/dist/.dts/presets/shared/missing-dependency.d.ts.map +0 -1
  83. package/dist/.dts/presets/shared/read-json-report.d.ts +0 -33
  84. package/dist/.dts/presets/shared/read-json-report.d.ts.map +0 -1
  85. package/dist/.dts/presets/shared/terminal-status.d.ts +0 -25
  86. package/dist/.dts/presets/shared/terminal-status.d.ts.map +0 -1
  87. package/dist/.dts/presets/shared/vitest-json-policy.d.ts +0 -20
  88. package/dist/.dts/presets/shared/vitest-json-policy.d.ts.map +0 -1
  89. package/dist/.dts/presets/stylelint.d.ts +0 -17
  90. package/dist/.dts/presets/stylelint.d.ts.map +0 -1
  91. package/dist/.dts/presets/test.d.ts +0 -4
  92. package/dist/.dts/presets/test.d.ts.map +0 -1
  93. package/dist/.dts/presets/typecheck.d.ts +0 -4
  94. package/dist/.dts/presets/typecheck.d.ts.map +0 -1
  95. package/dist/.dts/run-repo-contract.d.ts +0 -38
  96. package/dist/.dts/run-repo-contract.d.ts.map +0 -1
  97. package/dist/.dts/types.d.ts +0 -324
  98. package/dist/.dts/types.d.ts.map +0 -1
  99. package/dist/index.d.cts +0 -1
  100. package/dist/index.d.ts +0 -1
  101. package/dist/presets.d.cts +0 -1
  102. package/dist/presets.d.ts +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0](https://github.com/MaverickCER/repo-contract/compare/repo-contract-v0.2.1...repo-contract-v0.3.0) (2026-09-03)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * defineRepoContract/runRepoContract now require spawn and env on the config (e.g. spawn: child_process.spawn, env: process.env). See ADR 0011 and README's "Supplying spawn/env" section for migration.
9
+
10
+ ### Features
11
+
12
+ * contract engine, self-hosting checks and CI workflows ([97ae128](https://github.com/MaverickCER/repo-contract/commit/97ae128bd96f2c631057f1150068af58cc66b224))
13
+ * contract engine, self-hosting checks and CI workflows ([22b2a3b](https://github.com/MaverickCER/repo-contract/commit/22b2a3b03fe4e44ad7a01b82342d86583926416a))
14
+ * make process spawning and env access consumer-supplied capabilities ([640a961](https://github.com/MaverickCER/repo-contract/commit/640a961b65fd73511586c3b4f6274284bf5172a9))
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * clear Socket supply-chain alerts (unminified dist, no prepare script) ([71c9230](https://github.com/MaverickCER/repo-contract/commit/71c92306fcf00e3858f0596c2d56f3272199339d))
20
+ * publish the dist bundle unminified ([266ae47](https://github.com/MaverickCER/repo-contract/commit/266ae47919753c55f10364713fe0c7c5876815b7))
21
+ * remove the prepare install script from the published package ([9153540](https://github.com/MaverickCER/repo-contract/commit/9153540e4f0f1a069e07c12ddfee8ba62827ce5b))
22
+
23
+ ## [0.2.1](https://github.com/MaverickCER/repo-contract/compare/repo-contract-v0.2.0...repo-contract-v0.2.1) (2026-09-03)
24
+
25
+
26
+ ### Documentation
27
+
28
+ * add Socket security badge to README
29
+
30
+ ## [0.2.0](https://github.com/MaverickCER/repo-contract/compare/repo-contract-v0.1.1...repo-contract-v0.2.0) (2026-09-03)
31
+
32
+
33
+ ### ⚠ BREAKING CHANGES
34
+
35
+ * defineRepoContract/runRepoContract now require spawn and env on the config (e.g. spawn: child_process.spawn, env: process.env). See ADR 0011 and README's "Supplying spawn/env" section for migration.
36
+
37
+ ### Features
38
+
39
+ * make process spawning and env access consumer-supplied capabilities ([640a961](https://github.com/MaverickCER/repo-contract/commit/640a961b65fd73511586c3b4f6274284bf5172a9))
40
+
3
41
  ## [0.1.1](https://github.com/MaverickCER/repo-contract/compare/repo-contract-v0.1.0...repo-contract-v0.1.1) (2026-09-01)
4
42
 
5
43
 
package/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
  [![Coverage](https://img.shields.io/badge/coverage-%E2%89%A585%25-brightgreen)](scripts/coverage-thresholds.mjs)
7
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue)](tsconfig.json)
8
8
  [![Node](https://img.shields.io/node/v/repo-contract.svg)](package.json)
9
+ [![Socket Badge](https://badge.socket.dev/npm/package/repo-contract/latest)](https://badge.socket.dev/npm/package/repo-contract/latest)
9
10
 
10
11
  **Turn your repository's engineering standards into enforceable contracts.**
11
12
 
@@ -92,7 +93,7 @@ npm install --save-dev yaml
92
93
 
93
94
  ### Runtime support matrix
94
95
 
95
- repo-contract spawns processes and reads `process.env` it is server/CLI-only by design, not an isomorphic/browser package.
96
+ repo-contract's checks run as spawned processes and read the ambient environment — but repo-contract never imports a process-spawning implementation or reads `process.env` itself (see [Supplying `spawn`/`env`](#supplying-spawnenv) below); it is server/CLI-only by design regardless, not an isomorphic/browser package, since the capability a consumer supplies is itself always a real Node-only spawning mechanism.
96
97
 
97
98
  | Environment | Supported |
98
99
  | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -113,9 +114,14 @@ Define your repository's standards as checks:
113
114
 
114
115
  ```ts
115
116
  // repo-contract.config.ts
117
+ import { spawn } from "node:child_process"
116
118
  import { defineRepoContract } from "repo-contract"
117
119
 
118
120
  export default defineRepoContract({
121
+ // repo-contract never spawns a process or reads process.env itself -- see
122
+ // "Supplying spawn/env" below for why, and what to pass on Windows.
123
+ spawn,
124
+ env: process.env,
119
125
  checks: {
120
126
  tests: {
121
127
  run: "npm test",
@@ -179,6 +185,49 @@ process.exitCode = verdict.passed ? 0 : 1
179
185
 
180
186
  There is no CLI, no config-file discovery magic, and no hidden state.
181
187
 
188
+ ### Supplying `spawn`/`env`
189
+
190
+ `spawn` and `env` are required fields on the config — repo-contract never imports a process-spawning implementation or reads `process.env` internally (see [ADR 0011](specs/decisions/0011-process-spawning-and-ambient-environment-access-are-consumer-supplied-capabilities-not-package-owned.md)). You supply both, as trusted capabilities repo-contract calls with a resolved command/argv/options — it does not inspect, wrap, or sanitize them.
191
+
192
+ **macOS/Linux** — plain `node:child_process` is enough:
193
+
194
+ ```ts
195
+ import { spawn } from "node:child_process"
196
+
197
+ export default defineRepoContract({
198
+ spawn,
199
+ env: process.env,
200
+ checks: {/* ... */},
201
+ })
202
+ ```
203
+
204
+ **Windows** — most npm-installed CLI tools (`eslint`, `prettier`, `tsc`, …) resolve to `.cmd` shims, and plain `node:child_process.spawn` refuses to run those at all without `shell: true` (Node's own CVE-2024-27980 mitigation). Install [`cross-spawn`](https://www.npmjs.com/package/cross-spawn) and pass it instead — it resolves `.cmd`/`.bat` shims and quotes arguments safely for `cmd.exe`, **without** turning on shell metacharacter interpretation:
205
+
206
+ ```ts
207
+ import crossSpawn, { sync as crossSpawnSync } from "cross-spawn"
208
+
209
+ export default defineRepoContract({
210
+ spawn: crossSpawn,
211
+ env: process.env,
212
+ // Optional: lets a timed-out/aborted/Ctrl+C-killed check's full process
213
+ // tree (not just its immediate process) get cleaned up on Windows too.
214
+ killProcessTree: crossSpawnSync,
215
+ checks: {/* ... */},
216
+ })
217
+ ```
218
+
219
+ **`cross-spawn` does not mean "shell execution."** These are two independent choices:
220
+
221
+ | `Spawner` choice | `shell` option | Result |
222
+ | ---------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
223
+ | native `spawn` | `false` (default) | argv-only, no shell interpretation |
224
+ | `cross-spawn` | `false` (default) | Windows `.cmd`/`.bat` resolution + safe `cmd.exe` quoting, still argv-only |
225
+ | either | `true` | shell metacharacters (`&&`, `\|`, …) interpreted — a per-check or global opt-in, see [`shell`](docs/api-report/repo-contract.api.md) |
226
+
227
+ Passing `cross-spawn` fixes Windows command resolution; it does not by itself enable shell metacharacter interpretation. `check.shell` (or the config-level `shell` default) is the separate, explicit opt-in for that — see `SECURITY.md` before enabling it.
228
+
229
+ `repo-contract` doesn't ship a ready-made spawner of its own, on purpose (see ADR 0011's Alternatives) — the two snippets above are the whole integration.
230
+
182
231
  ## The model
183
232
 
184
233
  ```text
@@ -641,10 +690,13 @@ It does **not** encode your repository's definition of quality.
641
690
  Import a preset, spread it into your own `checks` record, and override whatever you need — most often `policy`:
642
691
 
643
692
  ```ts
693
+ import { spawn } from "node:child_process"
644
694
  import { defineRepoContract } from "repo-contract"
645
695
  import { format, typecheck, license } from "repo-contract/presets"
646
696
 
647
697
  export default defineRepoContract({
698
+ spawn,
699
+ env: process.env,
648
700
  checks: {
649
701
  format,
650
702
  typecheck: {
@@ -662,10 +714,13 @@ export default defineRepoContract({
662
714
  Some presets are factories because they expose options that change what gets executed:
663
715
 
664
716
  ```ts
717
+ import { spawn } from "node:child_process"
665
718
  import { defineRepoContract } from "repo-contract"
666
719
  import { lint, deadCode } from "repo-contract/presets"
667
720
 
668
721
  export default defineRepoContract({
722
+ spawn,
723
+ env: process.env,
669
724
  checks: {
670
725
  lint: lint({ path: "src" }),
671
726
  deadCode: deadCode({
package/dist/index.cjs CHANGED
@@ -1,11 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var os = require('os');
4
- var crossSpawn = require('cross-spawn');
5
4
  var string_decoder = require('string_decoder');
6
- var child_process = require('child_process');
7
-
8
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
5
 
10
6
  function _interopNamespace(e) {
11
7
  if (e && e.__esModule) return e;
@@ -26,7 +22,6 @@ function _interopNamespace(e) {
26
22
  }
27
23
 
28
24
  var os__namespace = /*#__PURE__*/_interopNamespace(os);
29
- var crossSpawn__default = /*#__PURE__*/_interopDefault(crossSpawn);
30
25
 
31
26
  // src/config/define-repo-contract.ts
32
27
  function defineRepoContract(config) {
@@ -236,7 +231,7 @@ function validateRepoContractConfig(config) {
236
231
  if (untrusted === null || typeof untrusted !== "object") {
237
232
  throw new InvalidRepoContractConfigError("config must be an object.");
238
233
  }
239
- const { checks, concurrency } = untrusted;
234
+ const { checks, concurrency, spawn, env, shell, killProcessTree } = untrusted;
240
235
  if (checks === null || typeof checks !== "object" || Array.isArray(checks)) {
241
236
  throw new InvalidRepoContractConfigError(
242
237
  "checks must be an object mapping check id to check definition."
@@ -249,12 +244,35 @@ function validateRepoContractConfig(config) {
249
244
  );
250
245
  }
251
246
  }
247
+ if (typeof spawn !== "function") {
248
+ throw new InvalidRepoContractConfigError("spawn must be a function.");
249
+ }
250
+ validateConfigEnv(env);
251
+ if (shell !== void 0 && typeof shell !== "boolean") {
252
+ throw new InvalidRepoContractConfigError("shell must be a boolean when provided.");
253
+ }
254
+ const globalShell = shell === true;
255
+ if (killProcessTree !== void 0 && typeof killProcessTree !== "function") {
256
+ throw new InvalidRepoContractConfigError("killProcessTree must be a function when provided.");
257
+ }
252
258
  for (const [checkId, check] of Object.entries(checks)) {
253
- validateCheckDefinition(checkId, check);
259
+ validateCheckDefinition(checkId, check, globalShell);
254
260
  }
255
261
  validateDependencyGraph(checks);
256
262
  }
257
- function validateCheckDefinition(checkId, check) {
263
+ function validateConfigEnv(env) {
264
+ if (env === null || typeof env !== "object" || Array.isArray(env)) {
265
+ throw new InvalidRepoContractConfigError(
266
+ "env must be an object mapping variable name to value."
267
+ );
268
+ }
269
+ for (const [key, value] of Object.entries(env)) {
270
+ if (typeof value !== "string" && value !== void 0) {
271
+ throw new InvalidRepoContractConfigError(`env["${key}"] must be a string or undefined.`);
272
+ }
273
+ }
274
+ }
275
+ function validateCheckDefinition(checkId, check, globalShell) {
258
276
  if (/^(?:0|[1-9]\d*)$/.test(checkId)) {
259
277
  throw new InvalidCheckConfigError(
260
278
  checkId,
@@ -265,7 +283,7 @@ function validateCheckDefinition(checkId, check) {
265
283
  throw new InvalidCheckConfigError(checkId, "check definition must be an object.");
266
284
  }
267
285
  const fields = check;
268
- const usesShell = validateShell(checkId, fields.shell);
286
+ const usesShell = validateShell(checkId, fields.shell, globalShell);
269
287
  validateRun(checkId, fields.run, usesShell);
270
288
  validateCwd(checkId, fields.cwd);
271
289
  validateEnv(checkId, fields.env);
@@ -276,11 +294,11 @@ function validateCheckDefinition(checkId, check) {
276
294
  validateIsolated(checkId, fields.isolated);
277
295
  validatePolicy(checkId, fields.policy);
278
296
  }
279
- function validateShell(checkId, shell) {
297
+ function validateShell(checkId, shell, globalShell) {
280
298
  if (shell !== void 0 && typeof shell !== "boolean") {
281
299
  throw new InvalidCheckConfigError(checkId, "shell must be a boolean when provided.");
282
300
  }
283
- return shell === true;
301
+ return typeof shell === "boolean" ? shell : globalShell;
284
302
  }
285
303
  function validateRun(checkId, run, usesShell) {
286
304
  if (typeof run !== "string" && !Array.isArray(run)) {
@@ -627,18 +645,23 @@ async function runWithConcurrencyGraph(items, concurrency, dependencyIndexes, wo
627
645
  launchNext();
628
646
  });
629
647
  }
648
+
649
+ // src/execution/process-tree.ts
630
650
  function shouldSpawnDetached() {
631
651
  return process.platform !== "win32";
632
652
  }
633
653
  function isErrnoException(error) {
634
654
  return error instanceof Error && "code" in error;
635
655
  }
636
- function killTree(pid, signal) {
656
+ function killTree(pid, signal, killProcessTree) {
637
657
  if (!Number.isInteger(pid) || pid <= 0) {
638
658
  return;
639
659
  }
640
660
  if (process.platform === "win32") {
641
- const result = child_process.spawnSync("taskkill", ["/pid", String(pid), "/t", "/f"], { stdio: "ignore" });
661
+ if (killProcessTree === void 0) return;
662
+ const result = killProcessTree("taskkill", ["/pid", String(pid), "/t", "/f"], {
663
+ stdio: "ignore"
664
+ });
642
665
  if (result.error !== void 0) throw result.error;
643
666
  return;
644
667
  }
@@ -672,7 +695,7 @@ function createBoundedCollector() {
672
695
  value: () => value
673
696
  };
674
697
  }
675
- function resolveCommand(checkId, check) {
698
+ function resolveCommand(checkId, check, effectiveShell) {
676
699
  const run = check.run;
677
700
  if (typeof run !== "string") {
678
701
  const [command2, ...args2] = run;
@@ -681,7 +704,7 @@ function resolveCommand(checkId, check) {
681
704
  }
682
705
  return { command: command2, args: args2 };
683
706
  }
684
- if (check.shell === true) {
707
+ if (effectiveShell) {
685
708
  return { command: run, args: [] };
686
709
  }
687
710
  const [command, ...args] = tokenizeRunString(run, checkId);
@@ -690,10 +713,10 @@ function resolveCommand(checkId, check) {
690
713
  }
691
714
  return { command, args };
692
715
  }
693
- function buildEnv(check) {
716
+ function buildEnv(check, ambientEnv) {
694
717
  const base = {};
695
718
  if (check.inheritEnv !== false) {
696
- for (const [key, value] of Object.entries(process.env)) {
719
+ for (const [key, value] of Object.entries(ambientEnv)) {
697
720
  if (value !== void 0) base[key] = value;
698
721
  }
699
722
  }
@@ -716,13 +739,14 @@ function terminalEvidence(command, args, startedAt, status, exitCode, signal, st
716
739
  ...spawnErrorCode !== void 0 ? { spawnErrorCode } : {}
717
740
  };
718
741
  }
719
- async function spawnCheck(checkId, check, runSignal, activeHandles) {
742
+ async function spawnCheck(checkId, check, runSignal, activeHandles, execution) {
720
743
  const startedAt = /* @__PURE__ */ new Date();
721
- const { command, args } = resolveCommand(checkId, check);
744
+ const effectiveShell = check.shell ?? execution.shell;
745
+ const { command, args } = resolveCommand(checkId, check, effectiveShell);
722
746
  if (runSignal?.aborted === true) {
723
747
  return terminalEvidence(command, args, startedAt, "aborted", null, null);
724
748
  }
725
- const env = buildEnv(check);
749
+ const env = buildEnv(check, execution.env);
726
750
  let terminationReason = null;
727
751
  let timeoutHandle;
728
752
  const timeoutController = new AbortController();
@@ -737,10 +761,10 @@ async function spawnCheck(checkId, check, runSignal, activeHandles) {
737
761
  runSignal !== void 0 ? [runSignal, timeoutController.signal] : [timeoutController.signal]
738
762
  );
739
763
  return new Promise((resolve) => {
740
- const child = crossSpawn__default.default(command, args, {
764
+ const child = execution.spawn(command, args, {
741
765
  cwd: check.cwd,
742
766
  env,
743
- shell: check.shell === true,
767
+ shell: effectiveShell,
744
768
  detached: shouldSpawnDetached(),
745
769
  // Windows-only cosmetic behavior (suppresses a console window flash)
746
770
  // with no effect on stdout/stderr/exitCode/signal on any platform, and
@@ -755,7 +779,11 @@ async function spawnCheck(checkId, check, runSignal, activeHandles) {
755
779
  let hostTerminated = false;
756
780
  const bestEffortKillTree = (pid, signal) => {
757
781
  try {
758
- killTree(pid, signal);
782
+ killTree(pid, signal, execution.killProcessTree);
783
+ } catch {
784
+ }
785
+ try {
786
+ child.kill(signal);
759
787
  } catch {
760
788
  }
761
789
  };
@@ -894,7 +922,7 @@ function resolveCheckDependencies(checks, requestedChecks) {
894
922
  for (const checkId of requestedChecks) visit(checkId);
895
923
  return Object.entries(checks).filter(([checkId]) => required.has(checkId));
896
924
  }
897
- async function runChecks(checks, concurrency, options) {
925
+ async function runChecks(checks, concurrency, execution, options) {
898
926
  const entries = options?.checks ? resolveCheckDependencies(checks, options.checks) : Object.entries(checks);
899
927
  const activeHandles = /* @__PURE__ */ new Set();
900
928
  const hostAbortController = new AbortController();
@@ -915,7 +943,7 @@ async function runChecks(checks, concurrency, options) {
915
943
  )
916
944
  );
917
945
  const worker = async ([checkId, check]) => {
918
- const evidence = await spawnCheck(checkId, check, runSignal, activeHandles);
946
+ const evidence = await spawnCheck(checkId, check, runSignal, activeHandles, execution);
919
947
  return [checkId, check, evidence];
920
948
  };
921
949
  try {
@@ -1047,7 +1075,13 @@ function runRepoContract(config, options) {
1047
1075
  async function runRepoContractAfterValidation(config, options) {
1048
1076
  const concurrency = config.concurrency ?? os__namespace.availableParallelism();
1049
1077
  const startedAt = /* @__PURE__ */ new Date();
1050
- const results = await runChecks(config.checks, concurrency, options);
1078
+ const execution = {
1079
+ spawn: config.spawn,
1080
+ killProcessTree: config.killProcessTree,
1081
+ env: config.env,
1082
+ shell: config.shell ?? false
1083
+ };
1084
+ const results = await runChecks(config.checks, concurrency, execution, options);
1051
1085
  const completedAt = /* @__PURE__ */ new Date();
1052
1086
  const { evidence, entries } = await buildEvidence(results, startedAt, completedAt);
1053
1087
  const verdict = await runPolicies(entries, evidence);