@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 +218 -94
- package/dist/index.js +235 -205
- package/package.json +1 -1
- package/src/exec-types.ts +64 -0
- package/src/gate-collectors.ts +30 -4
- package/src/run-containment.ts +38 -18
- package/src/verification.ts +240 -88
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
|
|
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(
|
|
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(
|
|
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
|
|
2798
|
-
var
|
|
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(
|
|
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(
|
|
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(
|
|
2915
|
+
log.info(TAG10, `[worker:${workerId}] Revert guard passed`);
|
|
2817
2916
|
}
|
|
2818
2917
|
}
|
|
2819
2918
|
if (config.verification.build) {
|
|
2820
|
-
log.info(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
2935
|
+
log.info(TAG10, `[worker:${workerId}] Tests passed`);
|
|
2837
2936
|
}
|
|
2838
2937
|
}
|
|
2839
2938
|
if (config.verification.lint) {
|
|
2840
|
-
log.info(
|
|
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(
|
|
2942
|
+
log.warn(TAG10, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
|
|
2844
2943
|
} else {
|
|
2845
|
-
log.info(
|
|
2944
|
+
log.info(TAG10, `[worker:${workerId}] Lint passed`);
|
|
2846
2945
|
}
|
|
2847
2946
|
}
|
|
2848
2947
|
if (config.verification.deepReview) {
|
|
2849
|
-
log.info(
|
|
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(
|
|
2951
|
+
log.warn(TAG10, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
|
|
2853
2952
|
} else {
|
|
2854
|
-
log.info(
|
|
2953
|
+
log.info(TAG10, `[worker:${workerId}] Deep review passed`);
|
|
2855
2954
|
}
|
|
2856
2955
|
}
|
|
2857
2956
|
return result;
|
|
2858
2957
|
}
|
|
2859
|
-
function
|
|
2860
|
-
const
|
|
2861
|
-
if (!
|
|
2862
|
-
|
|
2863
|
-
|
|
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:
|
|
2992
|
+
maxBuffer: MAX_OUTPUT_BUFFER3,
|
|
2871
2993
|
env: containedEnv()
|
|
2872
2994
|
});
|
|
2873
|
-
return
|
|
2995
|
+
return { ok: true };
|
|
2874
2996
|
} catch (err) {
|
|
2875
|
-
return
|
|
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(
|
|
3018
|
+
log.warn(TAG10, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
|
|
2882
3019
|
return [];
|
|
2883
3020
|
}
|
|
2884
|
-
|
|
2885
|
-
|
|
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
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
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
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
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(
|
|
3048
|
+
log.info(TAG10, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
|
|
2921
3049
|
return [];
|
|
2922
3050
|
}
|
|
2923
|
-
|
|
2924
|
-
|
|
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
|
-
|
|
2933
|
-
|
|
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(
|
|
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(
|
|
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:
|
|
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:
|
|
3121
|
+
maxBuffer: MAX_OUTPUT_BUFFER3,
|
|
2998
3122
|
env: containedEnv()
|
|
2999
3123
|
});
|
|
3000
3124
|
return parseReviewFindings(output);
|
|
3001
3125
|
} catch (err) {
|
|
3002
|
-
log.error(
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
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 ${
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
3892
|
+
log.warn(TAG13, `subagent error: ${event.payload.message}`);
|
|
3769
3893
|
} else {
|
|
3770
|
-
log.event(
|
|
3894
|
+
log.event(TAG13, `subagent ${event.kind}`);
|
|
3771
3895
|
}
|
|
3772
3896
|
}
|
|
3773
3897
|
} finally {
|