@gethmy/harness 1.5.0 → 1.6.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.
package/dist/index.js CHANGED
@@ -1128,9 +1128,9 @@ __export(exports_git_pr, {
1128
1128
  buildPrBody: () => buildPrBody
1129
1129
  });
1130
1130
  import { execFile as execFile2, execFileSync as execFileSync7 } from "node:child_process";
1131
- import { promisify as promisify2 } from "node:util";
1131
+ import { promisify as promisify3 } from "node:util";
1132
1132
  function createExecFileAsync2() {
1133
- return promisify2(execFile2);
1133
+ return promisify3(execFile2);
1134
1134
  }
1135
1135
  function execFileAsync2() {
1136
1136
  return cachedExecFileAsync2 ??= createExecFileAsync2();
@@ -1187,7 +1187,7 @@ function validateGitProviderCli(provider, cwd) {
1187
1187
  }
1188
1188
  case "bitbucket":
1189
1189
  case "unknown":
1190
- log.warn(TAG13, `Git provider "${provider}" — PR creation will be skipped (no CLI support)`);
1190
+ log.warn(TAG14, `Git provider "${provider}" — PR creation will be skipped (no CLI support)`);
1191
1191
  break;
1192
1192
  }
1193
1193
  }
@@ -1470,7 +1470,7 @@ async function checkPrMergeStatus(prUrl, cwd, provider) {
1470
1470
  try {
1471
1471
  parsed = JSON.parse(stdout.trim());
1472
1472
  } catch {
1473
- log.warn(TAG13, `Failed to parse glab JSON output for MR ${mrMatch[1]}`);
1473
+ log.warn(TAG14, `Failed to parse glab JSON output for MR ${mrMatch[1]}`);
1474
1474
  return "unknown";
1475
1475
  }
1476
1476
  if (typeof parsed !== "object" || parsed === null)
@@ -1568,7 +1568,7 @@ async function resolvePrHeadBranch(prUrl, cwd, provider) {
1568
1568
  const { stdout } = await execFileAsync2()("gh", ["pr", "view", prUrl, "--json", "headRefName,isCrossRepository"], { cwd, encoding: "utf-8", timeout: 1e4 });
1569
1569
  return decidePrBranch("github", stdout);
1570
1570
  } catch (err) {
1571
- log.warn(TAG13, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1571
+ log.warn(TAG14, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1572
1572
  return decidePrBranch("github", null);
1573
1573
  }
1574
1574
  }
@@ -1584,7 +1584,7 @@ async function resolvePrHeadBranch(prUrl, cwd, provider) {
1584
1584
  const { stdout } = await execFileAsync2()("az", ["repos", "pr", "show", "--id", prId, "--output", "json"], { cwd, encoding: "utf-8", timeout: 1e4 });
1585
1585
  return decidePrBranch("azure", stdout);
1586
1586
  } catch (err) {
1587
- log.warn(TAG13, `az repos pr show failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1587
+ log.warn(TAG14, `az repos pr show failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1588
1588
  return decidePrBranch("azure", null);
1589
1589
  }
1590
1590
  }
@@ -1626,7 +1626,7 @@ function remoteBranchExists(branchName, cwd) {
1626
1626
  }
1627
1627
  function pushBranch(branchName, cwd) {
1628
1628
  if (remoteBranchExists(branchName, cwd)) {
1629
- log.info(TAG13, `Remote branch ${branchName} exists (rework), force-pushing`);
1629
+ log.info(TAG14, `Remote branch ${branchName} exists (rework), force-pushing`);
1630
1630
  let expectedSha = null;
1631
1631
  try {
1632
1632
  execFileSync7("git", [...GIT_NO_HOOKS, "fetch", "origin", branchName], {
@@ -1635,7 +1635,7 @@ function pushBranch(branchName, cwd) {
1635
1635
  });
1636
1636
  expectedSha = execFileSync7("git", [...GIT_NO_HOOKS, "rev-parse", `refs/remotes/origin/${branchName}`], { cwd, encoding: "utf-8" }).trim();
1637
1637
  } catch (err) {
1638
- log.warn(TAG13, `could not resolve remote tip for ${branchName}, falling back to weak lease: ${err instanceof Error ? err.message : err}`);
1638
+ log.warn(TAG14, `could not resolve remote tip for ${branchName}, falling back to weak lease: ${err instanceof Error ? err.message : err}`);
1639
1639
  }
1640
1640
  const lease = expectedSha ? `--force-with-lease=refs/heads/${branchName}:${expectedSha}` : "--force-with-lease";
1641
1641
  execFileSync7("git", [...GIT_NO_HOOKS, "push", lease, "-u", "origin", branchName], {
@@ -1661,7 +1661,7 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
1661
1661
  } catch (err) {
1662
1662
  throw new Error(`renameRemoteBranch: could not resolve HEAD: ${err instanceof Error ? err.message : err}`);
1663
1663
  }
1664
- log.info(TAG13, `Renaming remote ${oldRef} → ${newRef}`);
1664
+ log.info(TAG14, `Renaming remote ${oldRef} → ${newRef}`);
1665
1665
  execFileSync7("git", [
1666
1666
  ...GIT_NO_HOOKS,
1667
1667
  "push",
@@ -1675,7 +1675,7 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
1675
1675
  stdio: "pipe"
1676
1676
  });
1677
1677
  } catch (err) {
1678
- log.warn(TAG13, `renameRemoteBranch: could not delete old ref ${oldRef}: ${err instanceof Error ? err.message : err}`);
1678
+ log.warn(TAG14, `renameRemoteBranch: could not delete old ref ${oldRef}: ${err instanceof Error ? err.message : err}`);
1679
1679
  }
1680
1680
  try {
1681
1681
  execFileSync7("git", [...GIT_NO_HOOKS, "branch", "-m", oldRef, newRef], {
@@ -1734,7 +1734,7 @@ function buildPrBody(card, commitLog) {
1734
1734
  }
1735
1735
  function createPullRequest(card, branchName, worktreePath, config, provider, existingPrUrl) {
1736
1736
  if (existingPrUrl) {
1737
- log.info(TAG13, `Reusing existing PR from card description: ${existingPrUrl}`);
1737
+ log.info(TAG14, `Reusing existing PR from card description: ${existingPrUrl}`);
1738
1738
  return existingPrUrl;
1739
1739
  }
1740
1740
  let commitLog = "";
@@ -1753,7 +1753,7 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
1753
1753
  const base = config.worktree.baseBranch;
1754
1754
  const existingUrl = findExistingPr(branchName, worktreePath, provider);
1755
1755
  if (existingUrl) {
1756
- log.info(TAG13, `PR already exists for ${branchName}, updating body...`);
1756
+ log.info(TAG14, `PR already exists for ${branchName}, updating body...`);
1757
1757
  updateExistingPr(branchName, body, worktreePath, provider);
1758
1758
  return existingUrl;
1759
1759
  }
@@ -1803,13 +1803,13 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
1803
1803
  ], { cwd: worktreePath, encoding: "utf-8" }).trim();
1804
1804
  break;
1805
1805
  default:
1806
- log.warn(TAG13, `No PR CLI for provider "${provider}" — branch pushed but no PR created`);
1806
+ log.warn(TAG14, `No PR CLI for provider "${provider}" — branch pushed but no PR created`);
1807
1807
  return null;
1808
1808
  }
1809
- log.info(TAG13, `PR created: ${result}`);
1809
+ log.info(TAG14, `PR created: ${result}`);
1810
1810
  return result;
1811
1811
  } catch (err) {
1812
- log.error(TAG13, `Failed to create PR: ${err instanceof Error ? err.message : err}`);
1812
+ log.error(TAG14, `Failed to create PR: ${err instanceof Error ? err.message : err}`);
1813
1813
  return null;
1814
1814
  }
1815
1815
  }
@@ -1843,12 +1843,12 @@ function updateExistingPr(branchName, body, worktreePath, provider) {
1843
1843
  execFileSync7("glab", ["mr", "update", branchName, "--description", body], { cwd: worktreePath, stdio: "pipe" });
1844
1844
  break;
1845
1845
  }
1846
- log.info(TAG13, `Updated existing PR body for ${branchName}`);
1846
+ log.info(TAG14, `Updated existing PR body for ${branchName}`);
1847
1847
  } catch (err) {
1848
- log.warn(TAG13, `Failed to update PR body: ${err instanceof Error ? err.message : err}`);
1848
+ log.warn(TAG14, `Failed to update PR body: ${err instanceof Error ? err.message : err}`);
1849
1849
  }
1850
1850
  }
1851
- var cachedExecFileAsync2, TAG13 = "git-pr", VALID_PR_URL_RE, PR_URL_RE, REVIEWED_SHA_RE, CI_REVIEW_REQUESTED_SHA_RE;
1851
+ var cachedExecFileAsync2, TAG14 = "git-pr", VALID_PR_URL_RE, PR_URL_RE, REVIEWED_SHA_RE, CI_REVIEW_REQUESTED_SHA_RE;
1852
1852
  var init_git_pr = __esm(() => {
1853
1853
  init_dist();
1854
1854
  init_log();
@@ -2721,6 +2721,7 @@ init_dist();
2721
2721
 
2722
2722
  // src/exec-types.ts
2723
2723
  var DEFAULT_METRIC_TIMEOUT_MS = 300000;
2724
+ var SANDBOX_STARTUP_GRACE_MS = 60000;
2724
2725
 
2725
2726
  // src/gate-config-error.ts
2726
2727
  init_dist();
@@ -3439,8 +3440,8 @@ function errText(err) {
3439
3440
  }
3440
3441
 
3441
3442
  // src/verification.ts
3442
- init_log();
3443
3443
  import { execFileSync as execFileSync5, spawn as spawn2 } from "node:child_process";
3444
+ init_log();
3444
3445
 
3445
3446
  // src/pm.ts
3446
3447
  init_log();
@@ -3672,11 +3673,108 @@ function resolveXcodeScheme(pt) {
3672
3673
  }
3673
3674
  }
3674
3675
 
3676
+ // src/repair-sandbox.ts
3677
+ init_log();
3678
+ import { randomUUID } from "node:crypto";
3679
+ import { promisify as promisify2 } from "node:util";
3680
+ var TAG9 = "repair-sandbox";
3681
+ async function dockerExec(argv, opts) {
3682
+ const { execFile: execFile2 } = await import("node:child_process");
3683
+ return promisify2(execFile2)("docker", argv, {
3684
+ encoding: "utf-8",
3685
+ ...opts
3686
+ });
3687
+ }
3688
+ var MAX_OUTPUT_BUFFER2 = 20971520;
3689
+ var PROBE_TIMEOUT_MS = 1e4;
3690
+ var SANDBOX_MOUNT = "/repo";
3691
+ var SANDBOX_MEMORY = "4g";
3692
+ var SANDBOX_PIDS = "512";
3693
+ var dockerProbe = null;
3694
+ async function sandboxAvailable() {
3695
+ if (dockerProbe === true)
3696
+ return true;
3697
+ try {
3698
+ await dockerExec(["version", "--format", "{{.Server.Version}}"], {
3699
+ timeout: PROBE_TIMEOUT_MS
3700
+ });
3701
+ dockerProbe = true;
3702
+ } catch {
3703
+ dockerProbe = false;
3704
+ }
3705
+ return dockerProbe;
3706
+ }
3707
+ function __resetSandboxProbe() {
3708
+ dockerProbe = null;
3709
+ }
3710
+ async function removeContainer(name) {
3711
+ try {
3712
+ await dockerExec(["rm", "--force", name], { timeout: PROBE_TIMEOUT_MS });
3713
+ log.warn(TAG9, `removed the timed-out sandbox container ${name}`);
3714
+ } catch {}
3715
+ }
3716
+ function sandboxRunArgs(image, worktree, command, name) {
3717
+ return [
3718
+ "run",
3719
+ "--rm",
3720
+ ...name ? ["--name", name] : [],
3721
+ "--network=none",
3722
+ "--cap-drop=ALL",
3723
+ "--security-opt=no-new-privileges",
3724
+ `--memory=${SANDBOX_MEMORY}`,
3725
+ `--pids-limit=${SANDBOX_PIDS}`,
3726
+ ...typeof process.getuid === "function" && typeof process.getgid === "function" ? ["--user", `${process.getuid()}:${process.getgid()}`] : [],
3727
+ "--env",
3728
+ "HOME=/tmp",
3729
+ "--volume",
3730
+ `${worktree}:${SANDBOX_MOUNT}`,
3731
+ "--workdir",
3732
+ SANDBOX_MOUNT,
3733
+ "--entrypoint",
3734
+ command.cmd,
3735
+ image,
3736
+ ...command.args
3737
+ ];
3738
+ }
3739
+ async function runInSandbox(args) {
3740
+ const name = `harmony-repair-${randomUUID()}`;
3741
+ const argv = sandboxRunArgs(args.image, args.worktree, args.command, name);
3742
+ log.info(TAG9, `sandbox: ${args.command.cmd} ${args.command.args.join(" ")} (image ${args.image})`);
3743
+ try {
3744
+ const { stdout } = await dockerExec(argv, {
3745
+ timeout: args.timeoutMs,
3746
+ maxBuffer: MAX_OUTPUT_BUFFER2
3747
+ });
3748
+ return { passed: true, output: stdout ?? "" };
3749
+ } catch (err) {
3750
+ const e = err;
3751
+ const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
3752
+ if (typeof e.code !== "number") {
3753
+ const timedOut = e.killed === true || e.signal != null;
3754
+ if (timedOut)
3755
+ await removeContainer(name);
3756
+ return {
3757
+ passed: false,
3758
+ output,
3759
+ sandboxError: timedOut ? `the sandbox timed out after ${args.timeoutMs}ms` : `the sandbox did not run: ${e.message ?? "unknown error"}`
3760
+ };
3761
+ }
3762
+ if (e.code === 125) {
3763
+ return {
3764
+ passed: false,
3765
+ output,
3766
+ sandboxError: `the sandbox could not start (image "${args.image}" missing or unusable)`
3767
+ };
3768
+ }
3769
+ return { passed: false, output };
3770
+ }
3771
+ }
3772
+
3675
3773
  // src/revert-guard.ts
3676
3774
  init_log();
3677
3775
  init_run_containment();
3678
3776
  import { execFileSync as execFileSync4 } from "node:child_process";
3679
- var TAG9 = "revert-guard";
3777
+ var TAG10 = "revert-guard";
3680
3778
  var TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
3681
3779
  function isTestFile(path) {
3682
3780
  return TEST_FILE.test(path);
@@ -3691,7 +3789,7 @@ function refetchBase(worktreePath, baseBranch) {
3691
3789
  stdio: "pipe"
3692
3790
  });
3693
3791
  } catch {
3694
- log.warn(TAG9, "Failed to re-fetch base for revert guard — using last fetch");
3792
+ log.warn(TAG10, "Failed to re-fetch base for revert guard — using last fetch");
3695
3793
  }
3696
3794
  }
3697
3795
  function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
@@ -3706,7 +3804,7 @@ function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
3706
3804
  return out.split(`
3707
3805
  `).map((l) => l.trim()).filter((l) => l.length > 0);
3708
3806
  } catch (err) {
3709
- log.warn(TAG9, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
3807
+ log.warn(TAG10, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
3710
3808
  return [];
3711
3809
  }
3712
3810
  }
@@ -3717,8 +3815,8 @@ function findDeletedTestFiles(worktreePath, baseBranch) {
3717
3815
 
3718
3816
  // src/verification.ts
3719
3817
  init_run_containment();
3720
- var TAG10 = "verification";
3721
- var MAX_OUTPUT_BUFFER2 = 64 * 1024 * 1024;
3818
+ var TAG11 = "verification";
3819
+ var MAX_OUTPUT_BUFFER3 = 64 * 1024 * 1024;
3722
3820
  async function runVerification(worktreePath, config, workerId) {
3723
3821
  const result = {
3724
3822
  passed: true,
@@ -3728,137 +3826,163 @@ async function runVerification(worktreePath, config, workerId) {
3728
3826
  reviewFindings: [],
3729
3827
  revertWarnings: []
3730
3828
  };
3829
+ const sandbox = verificationSandbox(config);
3731
3830
  if (config.verification.revertGuard) {
3732
- log.info(TAG10, `[worker:${workerId}] Checking for reverted merged work...`);
3831
+ log.info(TAG11, `[worker:${workerId}] Checking for reverted merged work...`);
3733
3832
  const deletedTests = findDeletedTestFiles(worktreePath, config.worktree.baseBranch);
3734
3833
  if (deletedTests.length > 0) {
3735
3834
  result.revertWarnings = deletedTests.map((f) => `Branch deletes test file '${f}' relative to current ${config.worktree.baseBranch} — ` + "likely an accidental revert of already-merged work. Restore the test or rebase on current main.");
3736
- log.warn(TAG10, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
3835
+ log.warn(TAG11, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
3737
3836
  result.passed = false;
3738
3837
  } else {
3739
- log.info(TAG10, `[worker:${workerId}] Revert guard passed`);
3838
+ log.info(TAG11, `[worker:${workerId}] Revert guard passed`);
3740
3839
  }
3741
3840
  }
3742
3841
  if (config.verification.build) {
3743
- log.info(TAG10, `[worker:${workerId}] Running build...`);
3744
- result.buildErrors = runBuild(worktreePath, config.verification.timeout);
3842
+ log.info(TAG11, `[worker:${workerId}] Running build...`);
3843
+ result.buildErrors = await runBuild(worktreePath, config.verification.timeout, sandbox);
3745
3844
  if (result.buildErrors.length > 0) {
3746
- log.warn(TAG10, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
3845
+ log.warn(TAG11, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
3747
3846
  result.passed = false;
3748
3847
  } else {
3749
- log.info(TAG10, `[worker:${workerId}] Build passed`);
3848
+ log.info(TAG11, `[worker:${workerId}] Build passed`);
3750
3849
  }
3751
3850
  }
3752
3851
  if (config.verification.test && result.buildErrors.length === 0) {
3753
- log.info(TAG10, `[worker:${workerId}] Running tests...`);
3754
- result.testFailures = runTests(worktreePath, config.verification.testTimeout);
3852
+ log.info(TAG11, `[worker:${workerId}] Running tests...`);
3853
+ result.testFailures = await runTests(worktreePath, config.verification.testTimeout, sandbox);
3755
3854
  if (result.testFailures.length > 0) {
3756
- log.warn(TAG10, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
3855
+ log.warn(TAG11, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
3757
3856
  result.passed = false;
3758
3857
  } else {
3759
- log.info(TAG10, `[worker:${workerId}] Tests passed`);
3858
+ log.info(TAG11, `[worker:${workerId}] Tests passed`);
3760
3859
  }
3761
3860
  }
3762
3861
  if (config.verification.lint) {
3763
- log.info(TAG10, `[worker:${workerId}] Running lint...`);
3764
- result.lintWarnings = runLint(worktreePath, config.verification.timeout);
3862
+ log.info(TAG11, `[worker:${workerId}] Running lint...`);
3863
+ result.lintWarnings = await runLint(worktreePath, config.verification.timeout, sandbox);
3765
3864
  if (result.lintWarnings.length > 0) {
3766
- log.warn(TAG10, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
3865
+ log.warn(TAG11, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
3767
3866
  } else {
3768
- log.info(TAG10, `[worker:${workerId}] Lint passed`);
3867
+ log.info(TAG11, `[worker:${workerId}] Lint passed`);
3769
3868
  }
3770
3869
  }
3771
3870
  if (config.verification.deepReview) {
3772
- log.info(TAG10, `[worker:${workerId}] Running deep review...`);
3871
+ log.info(TAG11, `[worker:${workerId}] Running deep review...`);
3773
3872
  result.reviewFindings = await runDeepReview(worktreePath, config, workerId);
3774
3873
  if (result.reviewFindings.length > 0) {
3775
- log.warn(TAG10, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
3874
+ log.warn(TAG11, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
3776
3875
  } else {
3777
- log.info(TAG10, `[worker:${workerId}] Deep review passed`);
3876
+ log.info(TAG11, `[worker:${workerId}] Deep review passed`);
3778
3877
  }
3779
3878
  }
3780
3879
  return result;
3781
3880
  }
3782
- function runBuild(worktreePath, timeout) {
3783
- const command = buildCommand(worktreePath);
3784
- if (!command) {
3785
- log.warn(TAG10, `No known build toolchain for ${worktreePath} — skipping build`);
3786
- return [];
3881
+ function verificationSandbox(config) {
3882
+ const image = config.verification.sandboxImage?.trim();
3883
+ if (!image)
3884
+ return;
3885
+ return { image };
3886
+ }
3887
+ async function execStep(command, args) {
3888
+ const { worktreePath, timeout } = args;
3889
+ const sandbox = args.sandbox?.image.trim() ? args.sandbox : undefined;
3890
+ if (sandbox) {
3891
+ if (!await sandboxAvailable()) {
3892
+ return {
3893
+ ok: false,
3894
+ sandboxError: `verification.sandboxImage is set to "${sandbox.image}" but no container runtime answered — ` + "start Docker or unset the image to verify on the host"
3895
+ };
3896
+ }
3897
+ const result = await runInSandbox({
3898
+ image: sandbox.image,
3899
+ worktree: worktreePath,
3900
+ command,
3901
+ timeoutMs: timeout + SANDBOX_STARTUP_GRACE_MS
3902
+ });
3903
+ if (result.sandboxError) {
3904
+ return { ok: false, sandboxError: result.sandboxError };
3905
+ }
3906
+ if (result.passed)
3907
+ return { ok: true };
3908
+ return { ok: false, err: { stdout: result.output, stderr: "" } };
3787
3909
  }
3788
3910
  try {
3789
3911
  execFileSync5(command.cmd, command.args, {
3790
3912
  cwd: worktreePath,
3791
3913
  timeout,
3792
3914
  stdio: "pipe",
3793
- maxBuffer: MAX_OUTPUT_BUFFER2,
3915
+ maxBuffer: MAX_OUTPUT_BUFFER3,
3794
3916
  env: containedEnv()
3795
3917
  });
3796
- return [];
3918
+ return { ok: true };
3797
3919
  } catch (err) {
3798
- return parseErrorOutput(err);
3920
+ return { ok: false, err };
3921
+ }
3922
+ }
3923
+ async function runBuild(worktreePath, timeout, sandbox) {
3924
+ const command = buildCommand(worktreePath);
3925
+ if (!command) {
3926
+ log.warn(TAG11, `No known build toolchain for ${worktreePath} — skipping build`);
3927
+ return [];
3799
3928
  }
3929
+ const outcome = await execStep(command, { worktreePath, timeout, sandbox });
3930
+ if (outcome.ok)
3931
+ return [];
3932
+ if (outcome.sandboxError) {
3933
+ log.error(TAG11, `Build not verified: ${outcome.sandboxError}`);
3934
+ return [`Build did not run: ${outcome.sandboxError}`];
3935
+ }
3936
+ return parseErrorOutput(outcome.err);
3800
3937
  }
3801
- function runTests(worktreePath, timeout) {
3938
+ async function runTests(worktreePath, timeout, sandbox) {
3802
3939
  const command = testCommand(worktreePath);
3803
3940
  if (!command) {
3804
- log.warn(TAG10, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
3941
+ log.warn(TAG11, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
3805
3942
  return [];
3806
3943
  }
3807
- try {
3808
- execFileSync5(command.cmd, command.args, {
3809
- cwd: worktreePath,
3810
- timeout,
3811
- stdio: "pipe",
3812
- maxBuffer: MAX_OUTPUT_BUFFER2,
3813
- env: containedEnv()
3814
- });
3944
+ const outcome = await execStep(command, { worktreePath, timeout, sandbox });
3945
+ if (outcome.ok)
3815
3946
  return [];
3816
- } catch (err) {
3817
- const output = combineOutput(err);
3818
- log.warn(TAG10, `Test run failed:
3819
- ${output.slice(-4000) || "(no output captured)"}`);
3820
- return parseTestFailures(err, timeout);
3947
+ if (outcome.sandboxError) {
3948
+ log.error(TAG11, `Tests not verified: ${outcome.sandboxError}`);
3949
+ return [`Test run did not happen: ${outcome.sandboxError}`];
3821
3950
  }
3951
+ const output = combineOutput(outcome.err);
3952
+ log.warn(TAG11, `Test run failed:
3953
+ ${output.slice(-4000) || "(no output captured)"}`);
3954
+ return parseTestFailures(outcome.err, timeout);
3822
3955
  }
3823
- function runFormatFix(worktreePath, timeout, workerId) {
3956
+ async function runFormatFix(worktreePath, timeout, workerId, sandbox) {
3824
3957
  const command = formatFixCommand(worktreePath);
3825
3958
  if (!command)
3826
3959
  return;
3827
- try {
3828
- execFileSync5(command.cmd, command.args, {
3829
- cwd: worktreePath,
3830
- timeout,
3831
- stdio: "pipe",
3832
- maxBuffer: MAX_OUTPUT_BUFFER2,
3833
- env: containedEnv()
3834
- });
3835
- log.info(TAG10, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
3836
- } catch (err) {
3837
- log.warn(TAG10, `[worker:${workerId}] Auto-format step exited non-zero (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
3960
+ const outcome = await execStep(command, { worktreePath, timeout, sandbox });
3961
+ if (outcome.ok) {
3962
+ log.info(TAG11, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
3963
+ return;
3838
3964
  }
3965
+ const why = outcome.sandboxError ? outcome.sandboxError : outcome.err instanceof Error ? outcome.err.message : String(outcome.err);
3966
+ log.warn(TAG11, `[worker:${workerId}] Auto-format step did not complete (non-fatal): ${why}`);
3839
3967
  }
3840
- function runLint(worktreePath, timeout) {
3968
+ async function runLint(worktreePath, timeout, sandbox) {
3841
3969
  const command = lintCommand(worktreePath);
3842
3970
  if (!command) {
3843
- log.info(TAG10, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
3971
+ log.info(TAG11, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
3844
3972
  return [];
3845
3973
  }
3846
- try {
3847
- execFileSync5(command.cmd, command.args, {
3848
- cwd: worktreePath,
3849
- timeout,
3850
- stdio: "pipe",
3851
- maxBuffer: MAX_OUTPUT_BUFFER2,
3852
- env: containedEnv()
3853
- });
3974
+ const outcome = await execStep(command, { worktreePath, timeout, sandbox });
3975
+ if (outcome.ok)
3854
3976
  return [];
3855
- } catch (err) {
3856
- return parseErrorOutput(err);
3977
+ if (outcome.sandboxError) {
3978
+ log.error(TAG11, `Lint not verified: ${outcome.sandboxError}`);
3979
+ return [`Lint did not run: ${outcome.sandboxError}`];
3857
3980
  }
3981
+ return parseErrorOutput(outcome.err);
3858
3982
  }
3859
3983
  async function runDeepReview(worktreePath, config, workerId) {
3860
3984
  if (!supportsDevServer(worktreePath)) {
3861
- log.info(TAG10, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
3985
+ log.info(TAG11, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
3862
3986
  return [];
3863
3987
  }
3864
3988
  const port = config.verification.devServerBasePort + workerId;
@@ -3874,7 +3998,7 @@ async function runDeepReview(worktreePath, config, workerId) {
3874
3998
  await waitForDevServer(devServer, 30000);
3875
3999
  await probeDevServer(port);
3876
4000
  } catch (err) {
3877
- log.error(TAG10, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
4001
+ log.error(TAG11, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
3878
4002
  return [];
3879
4003
  }
3880
4004
  let diff = "";
@@ -3883,7 +4007,7 @@ async function runDeepReview(worktreePath, config, workerId) {
3883
4007
  cwd: worktreePath,
3884
4008
  encoding: "utf-8",
3885
4009
  timeout: 30000,
3886
- maxBuffer: MAX_OUTPUT_BUFFER2
4010
+ maxBuffer: MAX_OUTPUT_BUFFER3
3887
4011
  });
3888
4012
  } catch {
3889
4013
  diff = "(unable to retrieve diff)";
@@ -3917,12 +4041,12 @@ async function runDeepReview(worktreePath, config, workerId) {
3917
4041
  encoding: "utf-8",
3918
4042
  timeout: config.verification.timeout,
3919
4043
  stdio: "pipe",
3920
- maxBuffer: MAX_OUTPUT_BUFFER2,
4044
+ maxBuffer: MAX_OUTPUT_BUFFER3,
3921
4045
  env: containedEnv()
3922
4046
  });
3923
4047
  return parseReviewFindings(output);
3924
4048
  } catch (err) {
3925
- log.error(TAG10, `Deep review failed: ${err instanceof Error ? err.message : err}`);
4049
+ log.error(TAG11, `Deep review failed: ${err instanceof Error ? err.message : err}`);
3926
4050
  return [];
3927
4051
  } finally {
3928
4052
  if (devServer && !devServer.killed) {
@@ -3960,12 +4084,12 @@ function attemptAutoFix(worktreePath, config, errors) {
3960
4084
  "--",
3961
4085
  fixPrompt
3962
4086
  ];
3963
- log.info(TAG10, "Spawning Claude for auto-fix...");
4087
+ log.info(TAG11, "Spawning Claude for auto-fix...");
3964
4088
  execFileSync5("claude", args, {
3965
4089
  cwd: worktreePath,
3966
4090
  timeout: config.verification.timeout,
3967
4091
  stdio: "pipe",
3968
- maxBuffer: MAX_OUTPUT_BUFFER2,
4092
+ maxBuffer: MAX_OUTPUT_BUFFER3,
3969
4093
  env: containedEnv()
3970
4094
  });
3971
4095
  }
@@ -3999,7 +4123,7 @@ async function reportFindings(client, cardId, result, recovery) {
3999
4123
  try {
4000
4124
  await client.createSubtask(cardId, title);
4001
4125
  } catch (err) {
4002
- log.error(TAG10, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
4126
+ log.error(TAG11, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
4003
4127
  }
4004
4128
  }));
4005
4129
  if (overflow > 0) {
@@ -4007,7 +4131,7 @@ async function reportFindings(client, cardId, result, recovery) {
4007
4131
  await client.createSubtask(cardId, `...and ${overflow} more issues`);
4008
4132
  } catch {}
4009
4133
  }
4010
- log.info(TAG10, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
4134
+ log.info(TAG11, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
4011
4135
  }
4012
4136
  function combineOutput(err) {
4013
4137
  const stderr = err?.stderr?.toString() ?? "";
@@ -4040,7 +4164,7 @@ function parseTestFailures(err, timeout) {
4040
4164
  }
4041
4165
  if (e?.code === "ENOBUFS") {
4042
4166
  return [
4043
- `Test output exceeded the ${MAX_OUTPUT_BUFFER2 / (1024 * 1024)}MB capture limit and the run was killed — ` + "the suite's real result is unknown. Quieten the reporter or raise the limit."
4167
+ `Test output exceeded the ${MAX_OUTPUT_BUFFER3 / (1024 * 1024)}MB capture limit and the run was killed — ` + "the suite's real result is unknown. Quieten the reporter or raise the limit."
4044
4168
  ];
4045
4169
  }
4046
4170
  const combined = combineOutput(err);
@@ -4131,7 +4255,7 @@ async function probeDevServer(port, timeoutMs = 5000) {
4131
4255
  }
4132
4256
 
4133
4257
  // src/gate-collectors.ts
4134
- var TAG11 = "gate-collectors";
4258
+ var TAG12 = "gate-collectors";
4135
4259
  async function resolveStageGate(client, card) {
4136
4260
  const currentStage = card.current_stage;
4137
4261
  const playbookId = card.playbook_id;
@@ -4149,7 +4273,7 @@ async function resolveStageGate(client, card) {
4149
4273
  return null;
4150
4274
  return { stage: resolution.stage, gate };
4151
4275
  } catch (err) {
4152
- log.warn(TAG11, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
4276
+ log.warn(TAG12, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
4153
4277
  return null;
4154
4278
  }
4155
4279
  }
@@ -4184,8 +4308,8 @@ class BuildGreenCollector {
4184
4308
  async collect(_context) {
4185
4309
  const doBuild = this.deps.runBuild ?? runBuild;
4186
4310
  const doLint = this.deps.runLint ?? runLint;
4187
- const buildErrors = doBuild(this.deps.worktreePath, this.deps.buildTimeout);
4188
- const lintWarnings = doLint(this.deps.worktreePath, this.deps.lintTimeout);
4311
+ const buildErrors = await doBuild(this.deps.worktreePath, this.deps.buildTimeout, this.deps.sandbox);
4312
+ const lintWarnings = await doLint(this.deps.worktreePath, this.deps.lintTimeout, this.deps.sandbox);
4189
4313
  const buildPassed = buildErrors.length === 0;
4190
4314
  const lintPassed = lintWarnings.length === 0;
4191
4315
  const result = buildPassed ? "passed" : "failed";
@@ -4281,7 +4405,7 @@ function buildGateCollectorRegistry(deps) {
4281
4405
  async function collectGateEvidence(registry, context) {
4282
4406
  const collector = registry[context.gate.kind];
4283
4407
  if (!collector) {
4284
- log.info(TAG11, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
4408
+ log.info(TAG12, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
4285
4409
  return {
4286
4410
  result: "blocked",
4287
4411
  structured: {
@@ -4293,7 +4417,7 @@ async function collectGateEvidence(registry, context) {
4293
4417
  return await collector.collect(context);
4294
4418
  } catch (err) {
4295
4419
  const msg = err instanceof Error ? err.message : String(err);
4296
- log.warn(TAG11, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
4420
+ log.warn(TAG12, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
4297
4421
  return { result: "blocked", structured: { error: msg } };
4298
4422
  }
4299
4423
  }
@@ -4301,7 +4425,7 @@ async function collectGateEvidence(registry, context) {
4301
4425
  init_log();
4302
4426
  init_run_containment();
4303
4427
  import { execFileSync as execFileSync6 } from "node:child_process";
4304
- var TAG12 = "git-diff-stat";
4428
+ var TAG13 = "git-diff-stat";
4305
4429
  var MAX_CHANGED_FILES = 30;
4306
4430
  function parseNumstat(raw, maxFiles = MAX_CHANGED_FILES) {
4307
4431
  const files = [];
@@ -4383,7 +4507,7 @@ function captureDiffStat(worktreePath, baseBranch, maxFiles = MAX_CHANGED_FILES)
4383
4507
  const raw = execFileSync6("git", [...GIT_NO_HOOKS, "diff", "--numstat", `${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
4384
4508
  return parseNumstat(raw, maxFiles);
4385
4509
  } catch (err) {
4386
- log.warn(TAG12, "git diff --numstat failed", {
4510
+ log.warn(TAG13, "git diff --numstat failed", {
4387
4511
  event: "diff_stat_failed",
4388
4512
  error: err instanceof Error ? err.message : String(err)
4389
4513
  });
@@ -4396,7 +4520,7 @@ init_git_pr();
4396
4520
 
4397
4521
  // src/harmony-client.ts
4398
4522
  init_log();
4399
- var TAG14 = "harmony-client";
4523
+ var TAG15 = "harmony-client";
4400
4524
  function readClientConfig(env) {
4401
4525
  const apiUrl = env.HARMONY_API_URL?.trim();
4402
4526
  const apiKey = env.HARMONY_API_KEY?.trim();
@@ -4448,7 +4572,7 @@ class HarmonyClient {
4448
4572
  purpose: "gate_evaluation"
4449
4573
  });
4450
4574
  if (!response.ok) {
4451
- log.warn(TAG14, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
4575
+ log.warn(TAG15, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
4452
4576
  return null;
4453
4577
  }
4454
4578
  const body = await response.json();
@@ -4528,102 +4652,6 @@ function relayAgentEvent(draft) {
4528
4652
  }
4529
4653
  return { type: "agent_event", event: draft };
4530
4654
  }
4531
- // src/repair-sandbox.ts
4532
- init_log();
4533
- import { randomUUID } from "node:crypto";
4534
- import { promisify as promisify3 } from "node:util";
4535
- var TAG15 = "repair-sandbox";
4536
- async function dockerExec(argv, opts) {
4537
- const { execFile: execFile3 } = await import("node:child_process");
4538
- return promisify3(execFile3)("docker", argv, {
4539
- encoding: "utf-8",
4540
- ...opts
4541
- });
4542
- }
4543
- var MAX_OUTPUT_BUFFER3 = 20971520;
4544
- var PROBE_TIMEOUT_MS = 1e4;
4545
- var SANDBOX_MOUNT = "/repo";
4546
- var SANDBOX_MEMORY = "4g";
4547
- var SANDBOX_PIDS = "512";
4548
- var dockerProbe = null;
4549
- async function sandboxAvailable() {
4550
- if (dockerProbe === true)
4551
- return true;
4552
- try {
4553
- await dockerExec(["version", "--format", "{{.Server.Version}}"], {
4554
- timeout: PROBE_TIMEOUT_MS
4555
- });
4556
- dockerProbe = true;
4557
- } catch {
4558
- dockerProbe = false;
4559
- }
4560
- return dockerProbe;
4561
- }
4562
- function __resetSandboxProbe() {
4563
- dockerProbe = null;
4564
- }
4565
- async function removeContainer(name) {
4566
- try {
4567
- await dockerExec(["rm", "--force", name], { timeout: PROBE_TIMEOUT_MS });
4568
- log.warn(TAG15, `removed the timed-out sandbox container ${name}`);
4569
- } catch {}
4570
- }
4571
- function sandboxRunArgs(image, worktree, command, name) {
4572
- return [
4573
- "run",
4574
- "--rm",
4575
- ...name ? ["--name", name] : [],
4576
- "--network=none",
4577
- "--cap-drop=ALL",
4578
- "--security-opt=no-new-privileges",
4579
- `--memory=${SANDBOX_MEMORY}`,
4580
- `--pids-limit=${SANDBOX_PIDS}`,
4581
- ...typeof process.getuid === "function" && typeof process.getgid === "function" ? ["--user", `${process.getuid()}:${process.getgid()}`] : [],
4582
- "--env",
4583
- "HOME=/tmp",
4584
- "--volume",
4585
- `${worktree}:${SANDBOX_MOUNT}`,
4586
- "--workdir",
4587
- SANDBOX_MOUNT,
4588
- "--entrypoint",
4589
- command.cmd,
4590
- image,
4591
- ...command.args
4592
- ];
4593
- }
4594
- async function runInSandbox(args) {
4595
- const name = `harmony-repair-${randomUUID()}`;
4596
- const argv = sandboxRunArgs(args.image, args.worktree, args.command, name);
4597
- log.info(TAG15, `sandbox: ${args.command.cmd} ${args.command.args.join(" ")} (image ${args.image})`);
4598
- try {
4599
- const { stdout } = await dockerExec(argv, {
4600
- timeout: args.timeoutMs,
4601
- maxBuffer: MAX_OUTPUT_BUFFER3
4602
- });
4603
- return { passed: true, output: stdout ?? "" };
4604
- } catch (err) {
4605
- const e = err;
4606
- const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
4607
- if (typeof e.code !== "number") {
4608
- const timedOut = e.killed === true || e.signal != null;
4609
- if (timedOut)
4610
- await removeContainer(name);
4611
- return {
4612
- passed: false,
4613
- output,
4614
- sandboxError: timedOut ? `the sandbox timed out after ${args.timeoutMs}ms` : `the sandbox did not run: ${e.message ?? "unknown error"}`
4615
- };
4616
- }
4617
- if (e.code === 125) {
4618
- return {
4619
- passed: false,
4620
- output,
4621
- sandboxError: `the sandbox could not start (image "${args.image}" missing or unusable)`
4622
- };
4623
- }
4624
- return { passed: false, output };
4625
- }
4626
- }
4627
4655
 
4628
4656
  // src/index.ts
4629
4657
  init_run_containment();
@@ -5202,6 +5230,7 @@ var MOTOR_NAME = "harmony-harness";
5202
5230
  export {
5203
5231
  writeOnlyDenyPaths,
5204
5232
  waitForDevServer,
5233
+ verificationSandbox,
5205
5234
  validateGitProviderCli,
5206
5235
  upsertReviewedSha,
5207
5236
  upsertCiReviewRequestedSha,
@@ -5343,6 +5372,7 @@ export {
5343
5372
  SdkAgentRunner,
5344
5373
  SIZING_MODEL,
5345
5374
  SDK_ALLOWED_TOOLS,
5375
+ SANDBOX_STARTUP_GRACE_MS,
5346
5376
  SANDBOX_MOUNT,
5347
5377
  ReviewPassedCollector,
5348
5378
  ProjectSandboxOverrideError,