@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/cli.js CHANGED
@@ -1802,6 +1802,7 @@ init_dist();
1802
1802
 
1803
1803
  // src/exec-types.ts
1804
1804
  var DEFAULT_METRIC_TIMEOUT_MS = 300000;
1805
+ var SANDBOX_STARTUP_GRACE_MS = 60000;
1805
1806
 
1806
1807
  // src/gate-config-error.ts
1807
1808
  init_dist();
@@ -2516,8 +2517,8 @@ function errText(err) {
2516
2517
  }
2517
2518
 
2518
2519
  // src/verification.ts
2519
- init_log();
2520
2520
  import { execFileSync as execFileSync4, spawn as spawn2 } from "node:child_process";
2521
+ init_log();
2521
2522
 
2522
2523
  // src/pm.ts
2523
2524
  init_log();
@@ -2749,11 +2750,108 @@ function resolveXcodeScheme(pt) {
2749
2750
  }
2750
2751
  }
2751
2752
 
2753
+ // src/repair-sandbox.ts
2754
+ init_log();
2755
+ import { randomUUID } from "node:crypto";
2756
+ import { promisify } from "node:util";
2757
+ var TAG8 = "repair-sandbox";
2758
+ async function dockerExec(argv, opts) {
2759
+ const { execFile } = await import("node:child_process");
2760
+ return promisify(execFile)("docker", argv, {
2761
+ encoding: "utf-8",
2762
+ ...opts
2763
+ });
2764
+ }
2765
+ var MAX_OUTPUT_BUFFER2 = 20971520;
2766
+ var PROBE_TIMEOUT_MS = 1e4;
2767
+ var SANDBOX_MOUNT = "/repo";
2768
+ var SANDBOX_MEMORY = "4g";
2769
+ var SANDBOX_PIDS = "512";
2770
+ var dockerProbe = null;
2771
+ async function sandboxAvailable() {
2772
+ if (dockerProbe === true)
2773
+ return true;
2774
+ try {
2775
+ await dockerExec(["version", "--format", "{{.Server.Version}}"], {
2776
+ timeout: PROBE_TIMEOUT_MS
2777
+ });
2778
+ dockerProbe = true;
2779
+ } catch {
2780
+ dockerProbe = false;
2781
+ }
2782
+ return dockerProbe;
2783
+ }
2784
+ function __resetSandboxProbe() {
2785
+ dockerProbe = null;
2786
+ }
2787
+ async function removeContainer(name) {
2788
+ try {
2789
+ await dockerExec(["rm", "--force", name], { timeout: PROBE_TIMEOUT_MS });
2790
+ log.warn(TAG8, `removed the timed-out sandbox container ${name}`);
2791
+ } catch {}
2792
+ }
2793
+ function sandboxRunArgs(image, worktree, command, name) {
2794
+ return [
2795
+ "run",
2796
+ "--rm",
2797
+ ...name ? ["--name", name] : [],
2798
+ "--network=none",
2799
+ "--cap-drop=ALL",
2800
+ "--security-opt=no-new-privileges",
2801
+ `--memory=${SANDBOX_MEMORY}`,
2802
+ `--pids-limit=${SANDBOX_PIDS}`,
2803
+ ...typeof process.getuid === "function" && typeof process.getgid === "function" ? ["--user", `${process.getuid()}:${process.getgid()}`] : [],
2804
+ "--env",
2805
+ "HOME=/tmp",
2806
+ "--volume",
2807
+ `${worktree}:${SANDBOX_MOUNT}`,
2808
+ "--workdir",
2809
+ SANDBOX_MOUNT,
2810
+ "--entrypoint",
2811
+ command.cmd,
2812
+ image,
2813
+ ...command.args
2814
+ ];
2815
+ }
2816
+ async function runInSandbox(args) {
2817
+ const name = `harmony-repair-${randomUUID()}`;
2818
+ const argv = sandboxRunArgs(args.image, args.worktree, args.command, name);
2819
+ log.info(TAG8, `sandbox: ${args.command.cmd} ${args.command.args.join(" ")} (image ${args.image})`);
2820
+ try {
2821
+ const { stdout } = await dockerExec(argv, {
2822
+ timeout: args.timeoutMs,
2823
+ maxBuffer: MAX_OUTPUT_BUFFER2
2824
+ });
2825
+ return { passed: true, output: stdout ?? "" };
2826
+ } catch (err) {
2827
+ const e = err;
2828
+ const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
2829
+ if (typeof e.code !== "number") {
2830
+ const timedOut = e.killed === true || e.signal != null;
2831
+ if (timedOut)
2832
+ await removeContainer(name);
2833
+ return {
2834
+ passed: false,
2835
+ output,
2836
+ sandboxError: timedOut ? `the sandbox timed out after ${args.timeoutMs}ms` : `the sandbox did not run: ${e.message ?? "unknown error"}`
2837
+ };
2838
+ }
2839
+ if (e.code === 125) {
2840
+ return {
2841
+ passed: false,
2842
+ output,
2843
+ sandboxError: `the sandbox could not start (image "${args.image}" missing or unusable)`
2844
+ };
2845
+ }
2846
+ return { passed: false, output };
2847
+ }
2848
+ }
2849
+
2752
2850
  // src/revert-guard.ts
2753
2851
  init_log();
2754
2852
  init_run_containment();
2755
2853
  import { execFileSync as execFileSync3 } from "node:child_process";
2756
- var TAG8 = "revert-guard";
2854
+ var TAG9 = "revert-guard";
2757
2855
  var TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
2758
2856
  function isTestFile(path) {
2759
2857
  return TEST_FILE.test(path);
@@ -2768,7 +2866,7 @@ function refetchBase(worktreePath, baseBranch) {
2768
2866
  stdio: "pipe"
2769
2867
  });
2770
2868
  } catch {
2771
- log.warn(TAG8, "Failed to re-fetch base for revert guard — using last fetch");
2869
+ log.warn(TAG9, "Failed to re-fetch base for revert guard — using last fetch");
2772
2870
  }
2773
2871
  }
2774
2872
  function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
@@ -2783,7 +2881,7 @@ function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
2783
2881
  return out.split(`
2784
2882
  `).map((l) => l.trim()).filter((l) => l.length > 0);
2785
2883
  } catch (err) {
2786
- log.warn(TAG8, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
2884
+ log.warn(TAG9, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
2787
2885
  return [];
2788
2886
  }
2789
2887
  }
@@ -2794,8 +2892,8 @@ function findDeletedTestFiles(worktreePath, baseBranch) {
2794
2892
 
2795
2893
  // src/verification.ts
2796
2894
  init_run_containment();
2797
- var TAG9 = "verification";
2798
- var MAX_OUTPUT_BUFFER2 = 64 * 1024 * 1024;
2895
+ var TAG10 = "verification";
2896
+ var MAX_OUTPUT_BUFFER3 = 64 * 1024 * 1024;
2799
2897
  async function runVerification(worktreePath, config, workerId) {
2800
2898
  const result = {
2801
2899
  passed: true,
@@ -2805,137 +2903,163 @@ async function runVerification(worktreePath, config, workerId) {
2805
2903
  reviewFindings: [],
2806
2904
  revertWarnings: []
2807
2905
  };
2906
+ const sandbox = verificationSandbox(config);
2808
2907
  if (config.verification.revertGuard) {
2809
- log.info(TAG9, `[worker:${workerId}] Checking for reverted merged work...`);
2908
+ log.info(TAG10, `[worker:${workerId}] Checking for reverted merged work...`);
2810
2909
  const deletedTests = findDeletedTestFiles(worktreePath, config.worktree.baseBranch);
2811
2910
  if (deletedTests.length > 0) {
2812
2911
  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.");
2813
- log.warn(TAG9, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
2912
+ log.warn(TAG10, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
2814
2913
  result.passed = false;
2815
2914
  } else {
2816
- log.info(TAG9, `[worker:${workerId}] Revert guard passed`);
2915
+ log.info(TAG10, `[worker:${workerId}] Revert guard passed`);
2817
2916
  }
2818
2917
  }
2819
2918
  if (config.verification.build) {
2820
- log.info(TAG9, `[worker:${workerId}] Running build...`);
2821
- result.buildErrors = runBuild(worktreePath, config.verification.timeout);
2919
+ log.info(TAG10, `[worker:${workerId}] Running build...`);
2920
+ result.buildErrors = await runBuild(worktreePath, config.verification.timeout, sandbox);
2822
2921
  if (result.buildErrors.length > 0) {
2823
- log.warn(TAG9, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
2922
+ log.warn(TAG10, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
2824
2923
  result.passed = false;
2825
2924
  } else {
2826
- log.info(TAG9, `[worker:${workerId}] Build passed`);
2925
+ log.info(TAG10, `[worker:${workerId}] Build passed`);
2827
2926
  }
2828
2927
  }
2829
2928
  if (config.verification.test && result.buildErrors.length === 0) {
2830
- log.info(TAG9, `[worker:${workerId}] Running tests...`);
2831
- result.testFailures = runTests(worktreePath, config.verification.testTimeout);
2929
+ log.info(TAG10, `[worker:${workerId}] Running tests...`);
2930
+ result.testFailures = await runTests(worktreePath, config.verification.testTimeout, sandbox);
2832
2931
  if (result.testFailures.length > 0) {
2833
- log.warn(TAG9, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
2932
+ log.warn(TAG10, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
2834
2933
  result.passed = false;
2835
2934
  } else {
2836
- log.info(TAG9, `[worker:${workerId}] Tests passed`);
2935
+ log.info(TAG10, `[worker:${workerId}] Tests passed`);
2837
2936
  }
2838
2937
  }
2839
2938
  if (config.verification.lint) {
2840
- log.info(TAG9, `[worker:${workerId}] Running lint...`);
2841
- result.lintWarnings = runLint(worktreePath, config.verification.timeout);
2939
+ log.info(TAG10, `[worker:${workerId}] Running lint...`);
2940
+ result.lintWarnings = await runLint(worktreePath, config.verification.timeout, sandbox);
2842
2941
  if (result.lintWarnings.length > 0) {
2843
- log.warn(TAG9, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
2942
+ log.warn(TAG10, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
2844
2943
  } else {
2845
- log.info(TAG9, `[worker:${workerId}] Lint passed`);
2944
+ log.info(TAG10, `[worker:${workerId}] Lint passed`);
2846
2945
  }
2847
2946
  }
2848
2947
  if (config.verification.deepReview) {
2849
- log.info(TAG9, `[worker:${workerId}] Running deep review...`);
2948
+ log.info(TAG10, `[worker:${workerId}] Running deep review...`);
2850
2949
  result.reviewFindings = await runDeepReview(worktreePath, config, workerId);
2851
2950
  if (result.reviewFindings.length > 0) {
2852
- log.warn(TAG9, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
2951
+ log.warn(TAG10, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
2853
2952
  } else {
2854
- log.info(TAG9, `[worker:${workerId}] Deep review passed`);
2953
+ log.info(TAG10, `[worker:${workerId}] Deep review passed`);
2855
2954
  }
2856
2955
  }
2857
2956
  return result;
2858
2957
  }
2859
- function runBuild(worktreePath, timeout) {
2860
- const command = buildCommand(worktreePath);
2861
- if (!command) {
2862
- log.warn(TAG9, `No known build toolchain for ${worktreePath} — skipping build`);
2863
- return [];
2958
+ function verificationSandbox(config) {
2959
+ const image = config.verification.sandboxImage?.trim();
2960
+ if (!image)
2961
+ return;
2962
+ return { image };
2963
+ }
2964
+ async function execStep(command, args) {
2965
+ const { worktreePath, timeout } = args;
2966
+ const sandbox = args.sandbox?.image.trim() ? args.sandbox : undefined;
2967
+ if (sandbox) {
2968
+ if (!await sandboxAvailable()) {
2969
+ return {
2970
+ ok: false,
2971
+ sandboxError: `verification.sandboxImage is set to "${sandbox.image}" but no container runtime answered — ` + "start Docker or unset the image to verify on the host"
2972
+ };
2973
+ }
2974
+ const result = await runInSandbox({
2975
+ image: sandbox.image,
2976
+ worktree: worktreePath,
2977
+ command,
2978
+ timeoutMs: timeout + SANDBOX_STARTUP_GRACE_MS
2979
+ });
2980
+ if (result.sandboxError) {
2981
+ return { ok: false, sandboxError: result.sandboxError };
2982
+ }
2983
+ if (result.passed)
2984
+ return { ok: true };
2985
+ return { ok: false, err: { stdout: result.output, stderr: "" } };
2864
2986
  }
2865
2987
  try {
2866
2988
  execFileSync4(command.cmd, command.args, {
2867
2989
  cwd: worktreePath,
2868
2990
  timeout,
2869
2991
  stdio: "pipe",
2870
- maxBuffer: MAX_OUTPUT_BUFFER2,
2992
+ maxBuffer: MAX_OUTPUT_BUFFER3,
2871
2993
  env: containedEnv()
2872
2994
  });
2873
- return [];
2995
+ return { ok: true };
2874
2996
  } catch (err) {
2875
- return parseErrorOutput(err);
2997
+ return { ok: false, err };
2998
+ }
2999
+ }
3000
+ async function runBuild(worktreePath, timeout, sandbox) {
3001
+ const command = buildCommand(worktreePath);
3002
+ if (!command) {
3003
+ log.warn(TAG10, `No known build toolchain for ${worktreePath} — skipping build`);
3004
+ return [];
2876
3005
  }
3006
+ const outcome = await execStep(command, { worktreePath, timeout, sandbox });
3007
+ if (outcome.ok)
3008
+ return [];
3009
+ if (outcome.sandboxError) {
3010
+ log.error(TAG10, `Build not verified: ${outcome.sandboxError}`);
3011
+ return [`Build did not run: ${outcome.sandboxError}`];
3012
+ }
3013
+ return parseErrorOutput(outcome.err);
2877
3014
  }
2878
- function runTests(worktreePath, timeout) {
3015
+ async function runTests(worktreePath, timeout, sandbox) {
2879
3016
  const command = testCommand(worktreePath);
2880
3017
  if (!command) {
2881
- log.warn(TAG9, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
3018
+ log.warn(TAG10, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
2882
3019
  return [];
2883
3020
  }
2884
- try {
2885
- execFileSync4(command.cmd, command.args, {
2886
- cwd: worktreePath,
2887
- timeout,
2888
- stdio: "pipe",
2889
- maxBuffer: MAX_OUTPUT_BUFFER2,
2890
- env: containedEnv()
2891
- });
3021
+ const outcome = await execStep(command, { worktreePath, timeout, sandbox });
3022
+ if (outcome.ok)
2892
3023
  return [];
2893
- } catch (err) {
2894
- const output = combineOutput(err);
2895
- log.warn(TAG9, `Test run failed:
2896
- ${output.slice(-4000) || "(no output captured)"}`);
2897
- return parseTestFailures(err, timeout);
3024
+ if (outcome.sandboxError) {
3025
+ log.error(TAG10, `Tests not verified: ${outcome.sandboxError}`);
3026
+ return [`Test run did not happen: ${outcome.sandboxError}`];
2898
3027
  }
3028
+ const output = combineOutput(outcome.err);
3029
+ log.warn(TAG10, `Test run failed:
3030
+ ${output.slice(-4000) || "(no output captured)"}`);
3031
+ return parseTestFailures(outcome.err, timeout);
2899
3032
  }
2900
- function runFormatFix(worktreePath, timeout, workerId) {
3033
+ async function runFormatFix(worktreePath, timeout, workerId, sandbox) {
2901
3034
  const command = formatFixCommand(worktreePath);
2902
3035
  if (!command)
2903
3036
  return;
2904
- try {
2905
- execFileSync4(command.cmd, command.args, {
2906
- cwd: worktreePath,
2907
- timeout,
2908
- stdio: "pipe",
2909
- maxBuffer: MAX_OUTPUT_BUFFER2,
2910
- env: containedEnv()
2911
- });
2912
- log.info(TAG9, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
2913
- } catch (err) {
2914
- log.warn(TAG9, `[worker:${workerId}] Auto-format step exited non-zero (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
3037
+ const outcome = await execStep(command, { worktreePath, timeout, sandbox });
3038
+ if (outcome.ok) {
3039
+ log.info(TAG10, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
3040
+ return;
2915
3041
  }
3042
+ const why = outcome.sandboxError ? outcome.sandboxError : outcome.err instanceof Error ? outcome.err.message : String(outcome.err);
3043
+ log.warn(TAG10, `[worker:${workerId}] Auto-format step did not complete (non-fatal): ${why}`);
2916
3044
  }
2917
- function runLint(worktreePath, timeout) {
3045
+ async function runLint(worktreePath, timeout, sandbox) {
2918
3046
  const command = lintCommand(worktreePath);
2919
3047
  if (!command) {
2920
- log.info(TAG9, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
3048
+ log.info(TAG10, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
2921
3049
  return [];
2922
3050
  }
2923
- try {
2924
- execFileSync4(command.cmd, command.args, {
2925
- cwd: worktreePath,
2926
- timeout,
2927
- stdio: "pipe",
2928
- maxBuffer: MAX_OUTPUT_BUFFER2,
2929
- env: containedEnv()
2930
- });
3051
+ const outcome = await execStep(command, { worktreePath, timeout, sandbox });
3052
+ if (outcome.ok)
2931
3053
  return [];
2932
- } catch (err) {
2933
- return parseErrorOutput(err);
3054
+ if (outcome.sandboxError) {
3055
+ log.error(TAG10, `Lint not verified: ${outcome.sandboxError}`);
3056
+ return [`Lint did not run: ${outcome.sandboxError}`];
2934
3057
  }
3058
+ return parseErrorOutput(outcome.err);
2935
3059
  }
2936
3060
  async function runDeepReview(worktreePath, config, workerId) {
2937
3061
  if (!supportsDevServer(worktreePath)) {
2938
- log.info(TAG9, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
3062
+ log.info(TAG10, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
2939
3063
  return [];
2940
3064
  }
2941
3065
  const port = config.verification.devServerBasePort + workerId;
@@ -2951,7 +3075,7 @@ async function runDeepReview(worktreePath, config, workerId) {
2951
3075
  await waitForDevServer(devServer, 30000);
2952
3076
  await probeDevServer(port);
2953
3077
  } catch (err) {
2954
- log.error(TAG9, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
3078
+ log.error(TAG10, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
2955
3079
  return [];
2956
3080
  }
2957
3081
  let diff = "";
@@ -2960,7 +3084,7 @@ async function runDeepReview(worktreePath, config, workerId) {
2960
3084
  cwd: worktreePath,
2961
3085
  encoding: "utf-8",
2962
3086
  timeout: 30000,
2963
- maxBuffer: MAX_OUTPUT_BUFFER2
3087
+ maxBuffer: MAX_OUTPUT_BUFFER3
2964
3088
  });
2965
3089
  } catch {
2966
3090
  diff = "(unable to retrieve diff)";
@@ -2994,12 +3118,12 @@ async function runDeepReview(worktreePath, config, workerId) {
2994
3118
  encoding: "utf-8",
2995
3119
  timeout: config.verification.timeout,
2996
3120
  stdio: "pipe",
2997
- maxBuffer: MAX_OUTPUT_BUFFER2,
3121
+ maxBuffer: MAX_OUTPUT_BUFFER3,
2998
3122
  env: containedEnv()
2999
3123
  });
3000
3124
  return parseReviewFindings(output);
3001
3125
  } catch (err) {
3002
- log.error(TAG9, `Deep review failed: ${err instanceof Error ? err.message : err}`);
3126
+ log.error(TAG10, `Deep review failed: ${err instanceof Error ? err.message : err}`);
3003
3127
  return [];
3004
3128
  } finally {
3005
3129
  if (devServer && !devServer.killed) {
@@ -3037,12 +3161,12 @@ function attemptAutoFix(worktreePath, config, errors) {
3037
3161
  "--",
3038
3162
  fixPrompt
3039
3163
  ];
3040
- log.info(TAG9, "Spawning Claude for auto-fix...");
3164
+ log.info(TAG10, "Spawning Claude for auto-fix...");
3041
3165
  execFileSync4("claude", args, {
3042
3166
  cwd: worktreePath,
3043
3167
  timeout: config.verification.timeout,
3044
3168
  stdio: "pipe",
3045
- maxBuffer: MAX_OUTPUT_BUFFER2,
3169
+ maxBuffer: MAX_OUTPUT_BUFFER3,
3046
3170
  env: containedEnv()
3047
3171
  });
3048
3172
  }
@@ -3076,7 +3200,7 @@ async function reportFindings(client, cardId, result, recovery) {
3076
3200
  try {
3077
3201
  await client.createSubtask(cardId, title);
3078
3202
  } catch (err) {
3079
- log.error(TAG9, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
3203
+ log.error(TAG10, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
3080
3204
  }
3081
3205
  }));
3082
3206
  if (overflow > 0) {
@@ -3084,7 +3208,7 @@ async function reportFindings(client, cardId, result, recovery) {
3084
3208
  await client.createSubtask(cardId, `...and ${overflow} more issues`);
3085
3209
  } catch {}
3086
3210
  }
3087
- log.info(TAG9, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
3211
+ log.info(TAG10, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
3088
3212
  }
3089
3213
  function combineOutput(err) {
3090
3214
  const stderr = err?.stderr?.toString() ?? "";
@@ -3117,7 +3241,7 @@ function parseTestFailures(err, timeout) {
3117
3241
  }
3118
3242
  if (e?.code === "ENOBUFS") {
3119
3243
  return [
3120
- `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."
3244
+ `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."
3121
3245
  ];
3122
3246
  }
3123
3247
  const combined = combineOutput(err);
@@ -3208,7 +3332,7 @@ async function probeDevServer(port, timeoutMs = 5000) {
3208
3332
  }
3209
3333
 
3210
3334
  // src/gate-collectors.ts
3211
- var TAG10 = "gate-collectors";
3335
+ var TAG11 = "gate-collectors";
3212
3336
  async function resolveStageGate(client, card) {
3213
3337
  const currentStage = card.current_stage;
3214
3338
  const playbookId = card.playbook_id;
@@ -3226,7 +3350,7 @@ async function resolveStageGate(client, card) {
3226
3350
  return null;
3227
3351
  return { stage: resolution.stage, gate };
3228
3352
  } catch (err) {
3229
- log.warn(TAG10, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
3353
+ log.warn(TAG11, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
3230
3354
  return null;
3231
3355
  }
3232
3356
  }
@@ -3261,8 +3385,8 @@ class BuildGreenCollector {
3261
3385
  async collect(_context) {
3262
3386
  const doBuild = this.deps.runBuild ?? runBuild;
3263
3387
  const doLint = this.deps.runLint ?? runLint;
3264
- const buildErrors = doBuild(this.deps.worktreePath, this.deps.buildTimeout);
3265
- const lintWarnings = doLint(this.deps.worktreePath, this.deps.lintTimeout);
3388
+ const buildErrors = await doBuild(this.deps.worktreePath, this.deps.buildTimeout, this.deps.sandbox);
3389
+ const lintWarnings = await doLint(this.deps.worktreePath, this.deps.lintTimeout, this.deps.sandbox);
3266
3390
  const buildPassed = buildErrors.length === 0;
3267
3391
  const lintPassed = lintWarnings.length === 0;
3268
3392
  const result = buildPassed ? "passed" : "failed";
@@ -3358,7 +3482,7 @@ function buildGateCollectorRegistry(deps) {
3358
3482
  async function collectGateEvidence(registry, context) {
3359
3483
  const collector = registry[context.gate.kind];
3360
3484
  if (!collector) {
3361
- log.info(TAG10, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
3485
+ log.info(TAG11, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
3362
3486
  return {
3363
3487
  result: "blocked",
3364
3488
  structured: {
@@ -3370,14 +3494,14 @@ async function collectGateEvidence(registry, context) {
3370
3494
  return await collector.collect(context);
3371
3495
  } catch (err) {
3372
3496
  const msg = err instanceof Error ? err.message : String(err);
3373
- log.warn(TAG10, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
3497
+ log.warn(TAG11, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
3374
3498
  return { result: "blocked", structured: { error: msg } };
3375
3499
  }
3376
3500
  }
3377
3501
 
3378
3502
  // src/harmony-client.ts
3379
3503
  init_log();
3380
- var TAG11 = "harmony-client";
3504
+ var TAG12 = "harmony-client";
3381
3505
  function readClientConfig(env) {
3382
3506
  const apiUrl = env.HARMONY_API_URL?.trim();
3383
3507
  const apiKey = env.HARMONY_API_KEY?.trim();
@@ -3429,7 +3553,7 @@ class HarmonyClient {
3429
3553
  purpose: "gate_evaluation"
3430
3554
  });
3431
3555
  if (!response.ok) {
3432
- log.warn(TAG11, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
3556
+ log.warn(TAG12, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
3433
3557
  return null;
3434
3558
  }
3435
3559
  const body = await response.json();
@@ -3713,7 +3837,7 @@ async function runStage(request, deps) {
3713
3837
  }
3714
3838
 
3715
3839
  // src/cli.ts
3716
- var TAG12 = "cli";
3840
+ var TAG13 = "cli";
3717
3841
  var GATE_VERIFICATION_TIMEOUT_MS = 600000;
3718
3842
  var SHUTDOWN_HARD_EXIT_MS = 15000;
3719
3843
  var activeRunner = null;
@@ -3744,12 +3868,12 @@ async function runRole(request, prompt, emit2) {
3744
3868
  });
3745
3869
  const runner = new SdkAgentRunner(launch.config);
3746
3870
  activeRunner = runner;
3747
- log.info(TAG12, `Running stage ${request.stageId} as role ${launch.role ?? "(none — fail-closed)"}`);
3871
+ log.info(TAG13, `Running stage ${request.stageId} as role ${launch.role ?? "(none — fail-closed)"}`);
3748
3872
  const timeoutMs = stageTimeoutMs(process.env);
3749
3873
  let timedOut = false;
3750
3874
  const clock = timeoutMs > 0 ? setTimeout(() => {
3751
3875
  timedOut = true;
3752
- log.warn(TAG12, `stage ${request.stageId} exceeded ${timeoutMs}ms — stopping the subagent`);
3876
+ log.warn(TAG13, `stage ${request.stageId} exceeded ${timeoutMs}ms — stopping the subagent`);
3753
3877
  runner.stop("timeout");
3754
3878
  }, timeoutMs) : null;
3755
3879
  clock?.unref?.();
@@ -3765,9 +3889,9 @@ async function runRole(request, prompt, emit2) {
3765
3889
  if (relayed)
3766
3890
  emit2(relayed);
3767
3891
  if (event.kind === "error") {
3768
- log.warn(TAG12, `subagent error: ${event.payload.message}`);
3892
+ log.warn(TAG13, `subagent error: ${event.payload.message}`);
3769
3893
  } else {
3770
- log.event(TAG12, `subagent ${event.kind}`);
3894
+ log.event(TAG13, `subagent ${event.kind}`);
3771
3895
  }
3772
3896
  }
3773
3897
  } finally {