@gethmy/harness 1.5.0 → 1.7.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 +497 -216
- package/dist/index.js +431 -254
- package/package.json +2 -2
- package/src/cli.ts +34 -2
- package/src/exec-types.ts +93 -0
- package/src/gate-collectors.ts +30 -4
- package/src/git-pr.ts +53 -5
- package/src/oracle-collector.ts +38 -8
- package/src/oracle.ts +464 -53
- package/src/repair-sandbox.test.ts +166 -1
- package/src/repair-sandbox.ts +203 -8
- package/src/run-containment.ts +194 -20
- package/src/stage-cli.ts +32 -8
- package/src/verification.ts +432 -101
- package/src/worktree.ts +19 -2
package/dist/index.js
CHANGED
|
@@ -100,14 +100,13 @@ var init_log = __esm(() => {
|
|
|
100
100
|
};
|
|
101
101
|
});
|
|
102
102
|
// ../harmony-shared/dist/agentStaleness.js
|
|
103
|
-
var AGENT_HEARTBEAT_LIVENESS_MS, AGENT_MILESTONE_LIVENESS_MS, AGENT_SWEEP_DAEMON_MS, AGENT_SWEEP_INTERACTIVE_MS, AGENT_SWEEP_PAUSED_MS,
|
|
103
|
+
var AGENT_HEARTBEAT_LIVENESS_MS, AGENT_MILESTONE_LIVENESS_MS, AGENT_SWEEP_DAEMON_MS, AGENT_SWEEP_INTERACTIVE_MS, AGENT_SWEEP_PAUSED_MS, ACTIVE_STATUSES;
|
|
104
104
|
var init_agentStaleness = __esm(() => {
|
|
105
105
|
AGENT_HEARTBEAT_LIVENESS_MS = 5 * 60 * 1000;
|
|
106
106
|
AGENT_MILESTONE_LIVENESS_MS = 30 * 60 * 1000;
|
|
107
107
|
AGENT_SWEEP_DAEMON_MS = 30 * 60 * 1000;
|
|
108
108
|
AGENT_SWEEP_INTERACTIVE_MS = 2 * 60 * 60 * 1000;
|
|
109
109
|
AGENT_SWEEP_PAUSED_MS = 4 * 60 * 60 * 1000;
|
|
110
|
-
SWEPT_SESSION_WRITE_GRACE_MS = 60 * 60 * 1000;
|
|
111
110
|
ACTIVE_STATUSES = new Set(["working", "blocked", "waiting"]);
|
|
112
111
|
});
|
|
113
112
|
// ../harmony-shared/dist/branchRef.js
|
|
@@ -862,6 +861,11 @@ function containedEnv(parentEnv = process.env) {
|
|
|
862
861
|
}
|
|
863
862
|
return out;
|
|
864
863
|
}
|
|
864
|
+
function heldTestEnvKeysToStrip(parentEnv = process.env) {
|
|
865
|
+
return [
|
|
866
|
+
...new Set([...secretEnvKeysToStrip(parentEnv), ...MODEL_CREDENTIAL_KEYS])
|
|
867
|
+
];
|
|
868
|
+
}
|
|
865
869
|
function gitMetadataDenyPaths(worktree) {
|
|
866
870
|
const paths = new Set;
|
|
867
871
|
const dotGit = join(worktree, ".git");
|
|
@@ -964,14 +968,20 @@ function implementRunToolPolicy(args) {
|
|
|
964
968
|
return allow;
|
|
965
969
|
};
|
|
966
970
|
}
|
|
967
|
-
function harmonyMcpServer() {
|
|
971
|
+
function harmonyMcpServer(run) {
|
|
968
972
|
const require2 = createRequire2(import.meta.url);
|
|
969
973
|
const cli = join(dirname2(require2.resolve("@gethmy/mcp")), "cli.js");
|
|
970
974
|
return {
|
|
971
975
|
harmony: {
|
|
972
976
|
type: "stdio",
|
|
973
977
|
command: process.execPath,
|
|
974
|
-
args: [cli, "serve"]
|
|
978
|
+
args: [cli, "serve"],
|
|
979
|
+
...run ? {
|
|
980
|
+
env: {
|
|
981
|
+
HARMONY_AGENT_CARD_ID: run.cardId,
|
|
982
|
+
HARMONY_AGENT_SESSION_ID: run.agentSessionId
|
|
983
|
+
}
|
|
984
|
+
} : {}
|
|
975
985
|
}
|
|
976
986
|
};
|
|
977
987
|
}
|
|
@@ -1004,7 +1014,7 @@ function implementRunContainment(args) {
|
|
|
1004
1014
|
canUseTool: implementRunToolPolicy(args),
|
|
1005
1015
|
gateEveryToolCall: true,
|
|
1006
1016
|
settingSources: args.readOnly === true ? [] : ["project"],
|
|
1007
|
-
mcpServers: harmonyMcpServer(),
|
|
1017
|
+
mcpServers: harmonyMcpServer(args.run),
|
|
1008
1018
|
strictMcpConfig: true,
|
|
1009
1019
|
stripEnvKeys: secretEnvKeysToStrip(),
|
|
1010
1020
|
disallowedTools: [
|
|
@@ -1028,7 +1038,7 @@ function implementRunContainmentCliArgs(args) {
|
|
|
1028
1038
|
containment.disallowedTools.join(",")
|
|
1029
1039
|
];
|
|
1030
1040
|
}
|
|
1031
|
-
var SECRET_ENV_PATTERN, KEEP_ENV_KEYS, ProjectSandboxOverrideError, INERT_PROJECT_SETTING_KEYS, WRITE_TOOL_PATHS, READ_TOOL_PATHS, GIT_NO_HOOKS, IMPLEMENT_ALLOWED_DOMAINS;
|
|
1041
|
+
var SECRET_ENV_PATTERN, KEEP_ENV_KEYS, ProjectSandboxOverrideError, INERT_PROJECT_SETTING_KEYS, MODEL_CREDENTIAL_KEYS, WRITE_TOOL_PATHS, READ_TOOL_PATHS, GIT_NO_HOOKS, IMPLEMENT_ALLOWED_DOMAINS;
|
|
1032
1042
|
var init_run_containment = __esm(() => {
|
|
1033
1043
|
init_confine_to_repo();
|
|
1034
1044
|
init_runner();
|
|
@@ -1065,6 +1075,11 @@ var init_run_containment = __esm(() => {
|
|
|
1065
1075
|
"theme",
|
|
1066
1076
|
"verbose"
|
|
1067
1077
|
]);
|
|
1078
|
+
MODEL_CREDENTIAL_KEYS = [
|
|
1079
|
+
"ANTHROPIC_API_KEY",
|
|
1080
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
1081
|
+
"CLAUDE_CODE_OAUTH_TOKEN"
|
|
1082
|
+
];
|
|
1068
1083
|
WRITE_TOOL_PATHS = {
|
|
1069
1084
|
Write: ["file_path"],
|
|
1070
1085
|
Edit: ["file_path"],
|
|
@@ -1128,9 +1143,9 @@ __export(exports_git_pr, {
|
|
|
1128
1143
|
buildPrBody: () => buildPrBody
|
|
1129
1144
|
});
|
|
1130
1145
|
import { execFile as execFile2, execFileSync as execFileSync7 } from "node:child_process";
|
|
1131
|
-
import { promisify as
|
|
1146
|
+
import { promisify as promisify3 } from "node:util";
|
|
1132
1147
|
function createExecFileAsync2() {
|
|
1133
|
-
return
|
|
1148
|
+
return promisify3(execFile2);
|
|
1134
1149
|
}
|
|
1135
1150
|
function execFileAsync2() {
|
|
1136
1151
|
return cachedExecFileAsync2 ??= createExecFileAsync2();
|
|
@@ -1187,7 +1202,7 @@ function validateGitProviderCli(provider, cwd) {
|
|
|
1187
1202
|
}
|
|
1188
1203
|
case "bitbucket":
|
|
1189
1204
|
case "unknown":
|
|
1190
|
-
log.warn(
|
|
1205
|
+
log.warn(TAG14, `Git provider "${provider}" — PR creation will be skipped (no CLI support)`);
|
|
1191
1206
|
break;
|
|
1192
1207
|
}
|
|
1193
1208
|
}
|
|
@@ -1470,7 +1485,7 @@ async function checkPrMergeStatus(prUrl, cwd, provider) {
|
|
|
1470
1485
|
try {
|
|
1471
1486
|
parsed = JSON.parse(stdout.trim());
|
|
1472
1487
|
} catch {
|
|
1473
|
-
log.warn(
|
|
1488
|
+
log.warn(TAG14, `Failed to parse glab JSON output for MR ${mrMatch[1]}`);
|
|
1474
1489
|
return "unknown";
|
|
1475
1490
|
}
|
|
1476
1491
|
if (typeof parsed !== "object" || parsed === null)
|
|
@@ -1568,7 +1583,7 @@ async function resolvePrHeadBranch(prUrl, cwd, provider) {
|
|
|
1568
1583
|
const { stdout } = await execFileAsync2()("gh", ["pr", "view", prUrl, "--json", "headRefName,isCrossRepository"], { cwd, encoding: "utf-8", timeout: 1e4 });
|
|
1569
1584
|
return decidePrBranch("github", stdout);
|
|
1570
1585
|
} catch (err) {
|
|
1571
|
-
log.warn(
|
|
1586
|
+
log.warn(TAG14, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1572
1587
|
return decidePrBranch("github", null);
|
|
1573
1588
|
}
|
|
1574
1589
|
}
|
|
@@ -1584,7 +1599,7 @@ async function resolvePrHeadBranch(prUrl, cwd, provider) {
|
|
|
1584
1599
|
const { stdout } = await execFileAsync2()("az", ["repos", "pr", "show", "--id", prId, "--output", "json"], { cwd, encoding: "utf-8", timeout: 1e4 });
|
|
1585
1600
|
return decidePrBranch("azure", stdout);
|
|
1586
1601
|
} catch (err) {
|
|
1587
|
-
log.warn(
|
|
1602
|
+
log.warn(TAG14, `az repos pr show failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1588
1603
|
return decidePrBranch("azure", null);
|
|
1589
1604
|
}
|
|
1590
1605
|
}
|
|
@@ -1626,7 +1641,7 @@ function remoteBranchExists(branchName, cwd) {
|
|
|
1626
1641
|
}
|
|
1627
1642
|
function pushBranch(branchName, cwd) {
|
|
1628
1643
|
if (remoteBranchExists(branchName, cwd)) {
|
|
1629
|
-
log.info(
|
|
1644
|
+
log.info(TAG14, `Remote branch ${branchName} exists (rework), force-pushing`);
|
|
1630
1645
|
let expectedSha = null;
|
|
1631
1646
|
try {
|
|
1632
1647
|
execFileSync7("git", [...GIT_NO_HOOKS, "fetch", "origin", branchName], {
|
|
@@ -1635,7 +1650,7 @@ function pushBranch(branchName, cwd) {
|
|
|
1635
1650
|
});
|
|
1636
1651
|
expectedSha = execFileSync7("git", [...GIT_NO_HOOKS, "rev-parse", `refs/remotes/origin/${branchName}`], { cwd, encoding: "utf-8" }).trim();
|
|
1637
1652
|
} catch (err) {
|
|
1638
|
-
log.warn(
|
|
1653
|
+
log.warn(TAG14, `could not resolve remote tip for ${branchName}, falling back to weak lease: ${err instanceof Error ? err.message : err}`);
|
|
1639
1654
|
}
|
|
1640
1655
|
const lease = expectedSha ? `--force-with-lease=refs/heads/${branchName}:${expectedSha}` : "--force-with-lease";
|
|
1641
1656
|
execFileSync7("git", [...GIT_NO_HOOKS, "push", lease, "-u", "origin", branchName], {
|
|
@@ -1661,7 +1676,7 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
|
|
|
1661
1676
|
} catch (err) {
|
|
1662
1677
|
throw new Error(`renameRemoteBranch: could not resolve HEAD: ${err instanceof Error ? err.message : err}`);
|
|
1663
1678
|
}
|
|
1664
|
-
log.info(
|
|
1679
|
+
log.info(TAG14, `Renaming remote ${oldRef} → ${newRef}`);
|
|
1665
1680
|
execFileSync7("git", [
|
|
1666
1681
|
...GIT_NO_HOOKS,
|
|
1667
1682
|
"push",
|
|
@@ -1675,7 +1690,7 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
|
|
|
1675
1690
|
stdio: "pipe"
|
|
1676
1691
|
});
|
|
1677
1692
|
} catch (err) {
|
|
1678
|
-
log.warn(
|
|
1693
|
+
log.warn(TAG14, `renameRemoteBranch: could not delete old ref ${oldRef}: ${err instanceof Error ? err.message : err}`);
|
|
1679
1694
|
}
|
|
1680
1695
|
try {
|
|
1681
1696
|
execFileSync7("git", [...GIT_NO_HOOKS, "branch", "-m", oldRef, newRef], {
|
|
@@ -1734,7 +1749,7 @@ function buildPrBody(card, commitLog) {
|
|
|
1734
1749
|
}
|
|
1735
1750
|
function createPullRequest(card, branchName, worktreePath, config, provider, existingPrUrl) {
|
|
1736
1751
|
if (existingPrUrl) {
|
|
1737
|
-
log.info(
|
|
1752
|
+
log.info(TAG14, `Reusing existing PR from card description: ${existingPrUrl}`);
|
|
1738
1753
|
return existingPrUrl;
|
|
1739
1754
|
}
|
|
1740
1755
|
let commitLog = "";
|
|
@@ -1753,7 +1768,7 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
|
|
|
1753
1768
|
const base = config.worktree.baseBranch;
|
|
1754
1769
|
const existingUrl = findExistingPr(branchName, worktreePath, provider);
|
|
1755
1770
|
if (existingUrl) {
|
|
1756
|
-
log.info(
|
|
1771
|
+
log.info(TAG14, `PR already exists for ${branchName}, updating body...`);
|
|
1757
1772
|
updateExistingPr(branchName, body, worktreePath, provider);
|
|
1758
1773
|
return existingUrl;
|
|
1759
1774
|
}
|
|
@@ -1803,13 +1818,13 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
|
|
|
1803
1818
|
], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
1804
1819
|
break;
|
|
1805
1820
|
default:
|
|
1806
|
-
log.warn(
|
|
1821
|
+
log.warn(TAG14, `No PR CLI for provider "${provider}" — branch pushed but no PR created`);
|
|
1807
1822
|
return null;
|
|
1808
1823
|
}
|
|
1809
|
-
log.info(
|
|
1824
|
+
log.info(TAG14, `PR created: ${result}`);
|
|
1810
1825
|
return result;
|
|
1811
1826
|
} catch (err) {
|
|
1812
|
-
log.error(
|
|
1827
|
+
log.error(TAG14, `Failed to create PR: ${err instanceof Error ? err.message : err}`);
|
|
1813
1828
|
return null;
|
|
1814
1829
|
}
|
|
1815
1830
|
}
|
|
@@ -1843,12 +1858,12 @@ function updateExistingPr(branchName, body, worktreePath, provider) {
|
|
|
1843
1858
|
execFileSync7("glab", ["mr", "update", branchName, "--description", body], { cwd: worktreePath, stdio: "pipe" });
|
|
1844
1859
|
break;
|
|
1845
1860
|
}
|
|
1846
|
-
log.info(
|
|
1861
|
+
log.info(TAG14, `Updated existing PR body for ${branchName}`);
|
|
1847
1862
|
} catch (err) {
|
|
1848
|
-
log.warn(
|
|
1863
|
+
log.warn(TAG14, `Failed to update PR body: ${err instanceof Error ? err.message : err}`);
|
|
1849
1864
|
}
|
|
1850
1865
|
}
|
|
1851
|
-
var cachedExecFileAsync2,
|
|
1866
|
+
var cachedExecFileAsync2, TAG14 = "git-pr", VALID_PR_URL_RE, PR_URL_RE, REVIEWED_SHA_RE, CI_REVIEW_REQUESTED_SHA_RE;
|
|
1852
1867
|
var init_git_pr = __esm(() => {
|
|
1853
1868
|
init_dist();
|
|
1854
1869
|
init_log();
|
|
@@ -2721,6 +2736,7 @@ init_dist();
|
|
|
2721
2736
|
|
|
2722
2737
|
// src/exec-types.ts
|
|
2723
2738
|
var DEFAULT_METRIC_TIMEOUT_MS = 300000;
|
|
2739
|
+
var SANDBOX_STARTUP_GRACE_MS = 60000;
|
|
2724
2740
|
|
|
2725
2741
|
// src/gate-config-error.ts
|
|
2726
2742
|
init_dist();
|
|
@@ -2988,7 +3004,8 @@ import { createHash as createHash2 } from "node:crypto";
|
|
|
2988
3004
|
init_log();
|
|
2989
3005
|
|
|
2990
3006
|
// src/oracle.ts
|
|
2991
|
-
import {
|
|
3007
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3008
|
+
import { lstatSync, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync } from "node:fs";
|
|
2992
3009
|
import {
|
|
2993
3010
|
chmod,
|
|
2994
3011
|
lstat,
|
|
@@ -3002,7 +3019,140 @@ import { tmpdir as tmpdir2 } from "node:os";
|
|
|
3002
3019
|
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
3003
3020
|
import { StringDecoder } from "node:string_decoder";
|
|
3004
3021
|
init_log();
|
|
3005
|
-
|
|
3022
|
+
|
|
3023
|
+
// src/repair-sandbox.ts
|
|
3024
|
+
init_log();
|
|
3025
|
+
import { randomUUID } from "node:crypto";
|
|
3026
|
+
import { promisify as promisify2 } from "node:util";
|
|
3027
|
+
var TAG5 = "repair-sandbox";
|
|
3028
|
+
async function dockerExec(argv, opts) {
|
|
3029
|
+
const { execFile: execFile2 } = await import("node:child_process");
|
|
3030
|
+
return promisify2(execFile2)("docker", argv, {
|
|
3031
|
+
encoding: "utf-8",
|
|
3032
|
+
...opts
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
var MAX_OUTPUT_BUFFER2 = 20971520;
|
|
3036
|
+
var PROBE_TIMEOUT_MS = 1e4;
|
|
3037
|
+
var SANDBOX_MOUNT = "/repo";
|
|
3038
|
+
var SANDBOX_MEMORY = "4g";
|
|
3039
|
+
var SANDBOX_PIDS = "512";
|
|
3040
|
+
var dockerProbe = null;
|
|
3041
|
+
async function sandboxAvailable() {
|
|
3042
|
+
if (dockerProbe === true)
|
|
3043
|
+
return true;
|
|
3044
|
+
try {
|
|
3045
|
+
await dockerExec(["version", "--format", "{{.Server.Version}}"], {
|
|
3046
|
+
timeout: PROBE_TIMEOUT_MS
|
|
3047
|
+
});
|
|
3048
|
+
dockerProbe = true;
|
|
3049
|
+
} catch {
|
|
3050
|
+
dockerProbe = false;
|
|
3051
|
+
}
|
|
3052
|
+
return dockerProbe;
|
|
3053
|
+
}
|
|
3054
|
+
function __resetSandboxProbe() {
|
|
3055
|
+
dockerProbe = null;
|
|
3056
|
+
}
|
|
3057
|
+
async function removeSandboxContainer(name) {
|
|
3058
|
+
try {
|
|
3059
|
+
await dockerExec(["rm", "--force", name], { timeout: PROBE_TIMEOUT_MS });
|
|
3060
|
+
log.info(TAG5, `removed sandbox container ${name}`);
|
|
3061
|
+
} catch {}
|
|
3062
|
+
}
|
|
3063
|
+
function sandboxRunArgs(image, worktree, command, name, mounts = []) {
|
|
3064
|
+
return [
|
|
3065
|
+
"run",
|
|
3066
|
+
"--rm",
|
|
3067
|
+
...name ? ["--name", name] : [],
|
|
3068
|
+
"--network=none",
|
|
3069
|
+
...sandboxHardeningArgs(worktree, mounts),
|
|
3070
|
+
"--entrypoint",
|
|
3071
|
+
command.cmd,
|
|
3072
|
+
image,
|
|
3073
|
+
...command.args
|
|
3074
|
+
];
|
|
3075
|
+
}
|
|
3076
|
+
function sandboxHardeningArgs(worktree, mounts = []) {
|
|
3077
|
+
return [
|
|
3078
|
+
"--cap-drop=ALL",
|
|
3079
|
+
"--security-opt=no-new-privileges",
|
|
3080
|
+
`--memory=${SANDBOX_MEMORY}`,
|
|
3081
|
+
`--pids-limit=${SANDBOX_PIDS}`,
|
|
3082
|
+
...typeof process.getuid === "function" && typeof process.getgid === "function" ? ["--user", `${process.getuid()}:${process.getgid()}`] : [],
|
|
3083
|
+
"--env",
|
|
3084
|
+
"HOME=/tmp",
|
|
3085
|
+
"--volume",
|
|
3086
|
+
`${worktree}:${SANDBOX_MOUNT}`,
|
|
3087
|
+
...mounts.flatMap((m) => ["--volume", `${m.host}:${m.container}`]),
|
|
3088
|
+
"--workdir",
|
|
3089
|
+
SANDBOX_MOUNT
|
|
3090
|
+
];
|
|
3091
|
+
}
|
|
3092
|
+
var SANDBOX_DEV_SERVER_BIND = "0.0.0.0";
|
|
3093
|
+
function devServerContainerName(port) {
|
|
3094
|
+
return `harmony-devserver-${port}`;
|
|
3095
|
+
}
|
|
3096
|
+
function devServerSandboxArgs(args) {
|
|
3097
|
+
return [
|
|
3098
|
+
"run",
|
|
3099
|
+
"--rm",
|
|
3100
|
+
"--quiet",
|
|
3101
|
+
"--name",
|
|
3102
|
+
args.name,
|
|
3103
|
+
"--publish",
|
|
3104
|
+
`127.0.0.1:${args.port}:${args.port}`,
|
|
3105
|
+
...sandboxHardeningArgs(args.worktree),
|
|
3106
|
+
"--entrypoint",
|
|
3107
|
+
args.command.cmd,
|
|
3108
|
+
args.image,
|
|
3109
|
+
...args.command.args
|
|
3110
|
+
];
|
|
3111
|
+
}
|
|
3112
|
+
async function runInSandbox(args) {
|
|
3113
|
+
const name = `harmony-repair-${randomUUID()}`;
|
|
3114
|
+
const argv = sandboxRunArgs(args.image, args.worktree, args.command, name);
|
|
3115
|
+
log.info(TAG5, `sandbox: ${args.command.cmd} ${args.command.args.join(" ")} (image ${args.image})`);
|
|
3116
|
+
try {
|
|
3117
|
+
const { stdout } = await dockerExec(argv, {
|
|
3118
|
+
timeout: args.timeoutMs,
|
|
3119
|
+
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3120
|
+
});
|
|
3121
|
+
return { passed: true, output: stdout ?? "" };
|
|
3122
|
+
} catch (err) {
|
|
3123
|
+
const e = err;
|
|
3124
|
+
const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
|
|
3125
|
+
if (typeof e.code !== "number") {
|
|
3126
|
+
const timedOut = e.killed === true || e.signal != null;
|
|
3127
|
+
if (timedOut)
|
|
3128
|
+
await removeSandboxContainer(name);
|
|
3129
|
+
return {
|
|
3130
|
+
passed: false,
|
|
3131
|
+
output,
|
|
3132
|
+
sandboxError: timedOut ? `the sandbox timed out after ${args.timeoutMs}ms` : `the sandbox did not run: ${e.message ?? "unknown error"}`
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
3135
|
+
if (e.code === 125) {
|
|
3136
|
+
return {
|
|
3137
|
+
passed: false,
|
|
3138
|
+
output,
|
|
3139
|
+
sandboxError: `the sandbox could not start (image "${args.image}" missing or unusable)`
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
return { passed: false, output };
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
// src/oracle.ts
|
|
3147
|
+
init_run_containment();
|
|
3148
|
+
var TAG6 = "oracle";
|
|
3149
|
+
|
|
3150
|
+
class OracleSandboxConfigError extends Error {
|
|
3151
|
+
constructor(message) {
|
|
3152
|
+
super(message);
|
|
3153
|
+
this.name = "OracleSandboxConfigError";
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3006
3156
|
async function resolveContained(repoPath, relativePath) {
|
|
3007
3157
|
if (isAbsolute3(relativePath)) {
|
|
3008
3158
|
throw new Error(`refusing to place an oracle at an absolute path: ${relativePath}`);
|
|
@@ -3133,12 +3283,24 @@ function gradeOracleRed(summary, exitCode) {
|
|
|
3133
3283
|
function argvPath(path) {
|
|
3134
3284
|
return path.startsWith("./") ? path : `./${path}`;
|
|
3135
3285
|
}
|
|
3136
|
-
async function runHeldOracle(repoPath, oracle,
|
|
3286
|
+
async function runHeldOracle(repoPath, oracle, options = {}) {
|
|
3287
|
+
const heldTestTimeoutMs = options.timeoutMs ?? DEFAULT_METRIC_TIMEOUT_MS;
|
|
3288
|
+
const sandbox = options.sandbox?.image.trim() ? options.sandbox : undefined;
|
|
3289
|
+
if (sandbox && !await sandboxAvailable()) {
|
|
3290
|
+
throw new OracleSandboxConfigError(`the held test is configured to run in "${sandbox.image}" but no container runtime answered — ` + "start Docker or unset verification.sandboxImage to run it on the host");
|
|
3291
|
+
}
|
|
3137
3292
|
const spec = resolveOracleRunnerSpec(oracle);
|
|
3138
3293
|
const reportDir = await mkdtemp(join2(tmpdir2(), reportDirPrefix()));
|
|
3139
3294
|
const reportPath = join2(reportDir, spec.report.file);
|
|
3140
3295
|
try {
|
|
3141
|
-
return await spawnHeldOracle(
|
|
3296
|
+
return await spawnHeldOracle({
|
|
3297
|
+
spec,
|
|
3298
|
+
repoPath,
|
|
3299
|
+
oracle,
|
|
3300
|
+
reportPath,
|
|
3301
|
+
timeoutMs: sandbox ? heldTestTimeoutMs + SANDBOX_STARTUP_GRACE_MS : heldTestTimeoutMs,
|
|
3302
|
+
sandbox
|
|
3303
|
+
});
|
|
3142
3304
|
} finally {
|
|
3143
3305
|
await removeReportDir(reportDir);
|
|
3144
3306
|
}
|
|
@@ -3159,23 +3321,57 @@ function assertUntampered(reportPath) {
|
|
|
3159
3321
|
throw new Error(`the oracle report is not owner-writable (mode ${(file.mode & 511).toString(8)}), so the runner could not have written it last — refusing it`);
|
|
3160
3322
|
}
|
|
3161
3323
|
}
|
|
3324
|
+
function assertConsistentWithExit(summary, exitCode) {
|
|
3325
|
+
if (!summary)
|
|
3326
|
+
return;
|
|
3327
|
+
if (summary.failed > 0 && exitCode === 0) {
|
|
3328
|
+
throw new Error(`the oracle report claims ${summary.failed} of ${summary.total} test(s) failed, but the runner exited 0 — ` + "neither allow-listed runner produces that pair, so the report is not the runner's; refusing it");
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3162
3331
|
async function removeReportDir(reportDir) {
|
|
3163
3332
|
try {
|
|
3164
3333
|
await chmod(reportDir, 448).catch(() => {});
|
|
3165
3334
|
await rm(reportDir, { recursive: true, force: true });
|
|
3166
3335
|
} catch (err) {
|
|
3167
|
-
log.warn(
|
|
3336
|
+
log.warn(TAG6, `Could not remove the oracle report directory ${reportDir} (${err instanceof Error ? err.message : String(err)}) — it may still hold the runner's report, which carries assertion text for vitest. Remove it by hand.`);
|
|
3168
3337
|
}
|
|
3169
3338
|
}
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3339
|
+
var ORACLE_REPORT_MOUNT = "/harmony-oracle-report";
|
|
3340
|
+
var DOCKER_SELF_FAILURE_CODES = new Set([125, 126, 127]);
|
|
3341
|
+
function heldOracleSandboxArgv(args) {
|
|
3342
|
+
const reportPathInContainer = join2(ORACLE_REPORT_MOUNT, args.reportFile);
|
|
3343
|
+
return {
|
|
3344
|
+
command: "docker",
|
|
3345
|
+
args: sandboxRunArgs(args.image, args.worktree, {
|
|
3346
|
+
cmd: args.runner.command,
|
|
3347
|
+
args: [...args.runner.args, ...args.reportFlags(reportPathInContainer)]
|
|
3348
|
+
}, args.containerName, [{ host: args.reportDir, container: ORACLE_REPORT_MOUNT }]),
|
|
3349
|
+
reportPathInContainer
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
3352
|
+
async function spawnHeldOracle(args_) {
|
|
3353
|
+
const { spec, repoPath, oracle, reportPath, timeoutMs, sandbox } = args_;
|
|
3354
|
+
const runner = spec.argv(argvPath(oracle.path));
|
|
3355
|
+
const containerName = sandbox ? `harmony-oracle-${randomUUID2()}` : null;
|
|
3356
|
+
const { command, args } = sandbox ? heldOracleSandboxArgv({
|
|
3357
|
+
image: sandbox.image,
|
|
3358
|
+
worktree: realpathSync2(repoPath),
|
|
3359
|
+
reportDir: realpathSync2(dirname3(reportPath)),
|
|
3360
|
+
reportFile: spec.report.file,
|
|
3361
|
+
runner,
|
|
3362
|
+
reportFlags: spec.report.flags,
|
|
3363
|
+
containerName: containerName ?? undefined
|
|
3364
|
+
}) : {
|
|
3365
|
+
command: runner.command,
|
|
3366
|
+
args: [...runner.args, ...spec.report.flags(reportPath)]
|
|
3367
|
+
};
|
|
3173
3368
|
return await new Promise((settleOk, settleErr) => {
|
|
3174
3369
|
let child;
|
|
3175
3370
|
try {
|
|
3176
3371
|
child = spawnInGroup(command, args, {
|
|
3177
3372
|
cwd: repoPath,
|
|
3178
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3373
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3374
|
+
stripEnvKeys: heldTestEnvKeysToStrip()
|
|
3179
3375
|
});
|
|
3180
3376
|
} catch (err) {
|
|
3181
3377
|
settleErr(err);
|
|
@@ -3207,18 +3403,20 @@ ${output}` : output;
|
|
|
3207
3403
|
return verdict;
|
|
3208
3404
|
};
|
|
3209
3405
|
let report = null;
|
|
3210
|
-
const captureReport = () => {
|
|
3406
|
+
const captureReport = (exitCode) => {
|
|
3211
3407
|
try {
|
|
3212
3408
|
assertUntampered(reportPath);
|
|
3213
|
-
|
|
3409
|
+
const parsed = spec.report.parse(readFileSync2(reportPath, "utf8"));
|
|
3410
|
+
assertConsistentWithExit(parsed, exitCode);
|
|
3411
|
+
report = parsed;
|
|
3214
3412
|
} catch (err) {
|
|
3215
3413
|
report = null;
|
|
3216
3414
|
const message = err instanceof Error ? err.message : String(err);
|
|
3217
3415
|
const absent = err instanceof Error && err.code === "ENOENT";
|
|
3218
3416
|
if (absent) {
|
|
3219
|
-
log.info(
|
|
3417
|
+
log.info(TAG6, `No oracle report at ${reportPath} — no verdict.`);
|
|
3220
3418
|
} else {
|
|
3221
|
-
log.warn(
|
|
3419
|
+
log.warn(TAG6, `Refusing the oracle report at ${reportPath}: ${message} — the gate will report no verdict.`);
|
|
3222
3420
|
}
|
|
3223
3421
|
}
|
|
3224
3422
|
};
|
|
@@ -3264,6 +3462,10 @@ ${output}` : output;
|
|
|
3264
3462
|
settle(new Error(`the held test was terminated by signal ${signal}`));
|
|
3265
3463
|
return;
|
|
3266
3464
|
}
|
|
3465
|
+
if (sandbox && DOCKER_SELF_FAILURE_CODES.has(code)) {
|
|
3466
|
+
settle(new OracleSandboxConfigError(`the held test's sandbox ("${sandbox.image}") exited ${code} without running the runner — ` + "the image is missing, unusable, or does not carry the runner; the motor's local log has docker's own message"));
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3267
3469
|
settle(null, {
|
|
3268
3470
|
exitCode: code,
|
|
3269
3471
|
output: finalOutput(),
|
|
@@ -3274,7 +3476,7 @@ ${output}` : output;
|
|
|
3274
3476
|
child.once("exit", (code, signal) => {
|
|
3275
3477
|
if (killing)
|
|
3276
3478
|
return;
|
|
3277
|
-
captureReport();
|
|
3479
|
+
captureReport(code);
|
|
3278
3480
|
if (timer)
|
|
3279
3481
|
clearTimeout(timer);
|
|
3280
3482
|
reapGroup(pgid);
|
|
@@ -3288,7 +3490,9 @@ ${output}` : output;
|
|
|
3288
3490
|
terminateGroup(child, {
|
|
3289
3491
|
sigintTimeoutMs: ORACLE_SIGINT_GRACE_MS,
|
|
3290
3492
|
sigtermTimeoutMs: ORACLE_SIGTERM_GRACE_MS
|
|
3291
|
-
}).catch(() => {}).then(() => {
|
|
3493
|
+
}).catch(() => {}).then(async () => {
|
|
3494
|
+
if (containerName)
|
|
3495
|
+
await removeSandboxContainer(containerName);
|
|
3292
3496
|
settle(new Error(`the held test did not finish within ${timeoutMs}ms`));
|
|
3293
3497
|
});
|
|
3294
3498
|
}, timeoutMs);
|
|
@@ -3296,7 +3500,7 @@ ${output}` : output;
|
|
|
3296
3500
|
}
|
|
3297
3501
|
|
|
3298
3502
|
// src/oracle-collector.ts
|
|
3299
|
-
var
|
|
3503
|
+
var TAG7 = "oracle-collector";
|
|
3300
3504
|
|
|
3301
3505
|
class HeldOracleCollector {
|
|
3302
3506
|
deps;
|
|
@@ -3307,7 +3511,7 @@ class HeldOracleCollector {
|
|
|
3307
3511
|
const stageId = this.oracleStageId(context);
|
|
3308
3512
|
if (!stageId) {
|
|
3309
3513
|
const reason = `No stage downstream of ${context.stageId} declares an \`oracle_passed\` gate, ` + "so there is no held test for this gate to grade. A red gate needs a later stage that runs the same test green.";
|
|
3310
|
-
log.warn(
|
|
3514
|
+
log.warn(TAG7, `${reason} — blocked (config)`);
|
|
3311
3515
|
return {
|
|
3312
3516
|
result: "blocked",
|
|
3313
3517
|
structured: { reason, ...GATE_CONFIG_ERROR_MARK }
|
|
@@ -3315,7 +3519,7 @@ class HeldOracleCollector {
|
|
|
3315
3519
|
}
|
|
3316
3520
|
const oracle = await this.deps.fetchOracle(context.cardId, stageId, this.deps.sessionId);
|
|
3317
3521
|
if (!oracle) {
|
|
3318
|
-
log.info(
|
|
3522
|
+
log.info(TAG7, `No oracle held for stage ${stageId} — blocked`);
|
|
3319
3523
|
return {
|
|
3320
3524
|
result: "blocked",
|
|
3321
3525
|
structured: {
|
|
@@ -3332,13 +3536,13 @@ class HeldOracleCollector {
|
|
|
3332
3536
|
};
|
|
3333
3537
|
await this.deps.place(this.deps.repoPath, oracle);
|
|
3334
3538
|
try {
|
|
3335
|
-
const { exitCode, output, report } = await this.deps.run(this.deps.repoPath, oracle);
|
|
3539
|
+
const { exitCode, output, report } = await this.deps.run(this.deps.repoPath, oracle, { sandbox: this.deps.sandbox });
|
|
3336
3540
|
const logLine = `Oracle run for ${oracle.path} exited ${exitCode}:
|
|
3337
3541
|
${output}`;
|
|
3338
3542
|
if (exitCode === 0) {
|
|
3339
|
-
log.info(
|
|
3543
|
+
log.info(TAG7, logLine);
|
|
3340
3544
|
} else {
|
|
3341
|
-
log.warn(
|
|
3545
|
+
log.warn(TAG7, logLine);
|
|
3342
3546
|
}
|
|
3343
3547
|
return this.verdict({
|
|
3344
3548
|
oracle,
|
|
@@ -3349,12 +3553,14 @@ ${output}`;
|
|
|
3349
3553
|
});
|
|
3350
3554
|
} catch (err) {
|
|
3351
3555
|
const message = errText(err);
|
|
3352
|
-
|
|
3556
|
+
const configError2 = err instanceof OracleSandboxConfigError;
|
|
3557
|
+
log.warn(TAG7, `Oracle run threw: ${message} — blocked${configError2 ? " (config)" : ""}`);
|
|
3353
3558
|
return {
|
|
3354
3559
|
result: "blocked",
|
|
3355
3560
|
structured: {
|
|
3356
3561
|
oracle: { path: oracle.path, ...identity },
|
|
3357
|
-
error: message
|
|
3562
|
+
error: message,
|
|
3563
|
+
...configError2 ? { reason: message, ...GATE_CONFIG_ERROR_MARK } : {}
|
|
3358
3564
|
}
|
|
3359
3565
|
};
|
|
3360
3566
|
} finally {
|
|
@@ -3366,13 +3572,13 @@ ${output}`;
|
|
|
3366
3572
|
await this.deps.remove(this.deps.repoPath, oracle);
|
|
3367
3573
|
return;
|
|
3368
3574
|
} catch (err) {
|
|
3369
|
-
log.warn(
|
|
3575
|
+
log.warn(TAG7, `Removing the held test at ${oracle.path} failed (${errText(err)}) — retrying once`);
|
|
3370
3576
|
}
|
|
3371
3577
|
try {
|
|
3372
3578
|
await this.deps.remove(this.deps.repoPath, oracle);
|
|
3373
|
-
log.info(
|
|
3579
|
+
log.info(TAG7, `Held test at ${oracle.path} removed on the second attempt`);
|
|
3374
3580
|
} catch (err) {
|
|
3375
|
-
log.error(
|
|
3581
|
+
log.error(TAG7, `HELD TEST NOT REMOVED: ${oracle.path} is still in ${this.deps.repoPath} after two attempts (${errText(err)}). ` + "It will be auto-committed by the completion path if it is left there — delete it by hand and check whether it reached a commit.");
|
|
3376
3582
|
}
|
|
3377
3583
|
}
|
|
3378
3584
|
}
|
|
@@ -3415,7 +3621,7 @@ class OracleRedCollector extends HeldOracleCollector {
|
|
|
3415
3621
|
const graded = gradeOracleRed(report, exitCode);
|
|
3416
3622
|
const base = { exitCode, path: oracle.path, ...identity };
|
|
3417
3623
|
if (graded.outcome === "no_verdict") {
|
|
3418
|
-
log.warn(
|
|
3624
|
+
log.warn(TAG7, `Red gate for ${oracle.path}: ${graded.reason} — blocked`);
|
|
3419
3625
|
return {
|
|
3420
3626
|
result: "blocked",
|
|
3421
3627
|
structured: { oracle: base, reason: graded.reason }
|
|
@@ -3439,15 +3645,15 @@ function errText(err) {
|
|
|
3439
3645
|
}
|
|
3440
3646
|
|
|
3441
3647
|
// src/verification.ts
|
|
3442
|
-
init_log();
|
|
3443
3648
|
import { execFileSync as execFileSync5, spawn as spawn2 } from "node:child_process";
|
|
3649
|
+
init_log();
|
|
3444
3650
|
|
|
3445
3651
|
// src/pm.ts
|
|
3446
3652
|
init_log();
|
|
3447
3653
|
init_run_containment();
|
|
3448
3654
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3449
3655
|
import { existsSync } from "node:fs";
|
|
3450
|
-
var
|
|
3656
|
+
var TAG8 = "pm";
|
|
3451
3657
|
var cached = null;
|
|
3452
3658
|
function detectPackageManager() {
|
|
3453
3659
|
if (cached)
|
|
@@ -3469,7 +3675,7 @@ function detectPackageManager() {
|
|
|
3469
3675
|
} else {
|
|
3470
3676
|
cached = "npm";
|
|
3471
3677
|
}
|
|
3472
|
-
log.info(
|
|
3678
|
+
log.info(TAG8, `Detected package manager: ${cached}`);
|
|
3473
3679
|
return cached;
|
|
3474
3680
|
}
|
|
3475
3681
|
function installCommand(ignoreScripts = false) {
|
|
@@ -3498,7 +3704,7 @@ function spawnRunArgs(script, ...extra) {
|
|
|
3498
3704
|
init_log();
|
|
3499
3705
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
3500
3706
|
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
3501
|
-
var
|
|
3707
|
+
var TAG9 = "project-type";
|
|
3502
3708
|
var _cache = new Map;
|
|
3503
3709
|
function _resetCache() {
|
|
3504
3710
|
_cache.clear();
|
|
@@ -3509,7 +3715,7 @@ function detect(dir) {
|
|
|
3509
3715
|
return cached2;
|
|
3510
3716
|
const result = detectUncached(dir);
|
|
3511
3717
|
_cache.set(dir, result);
|
|
3512
|
-
log.info(
|
|
3718
|
+
log.info(TAG9, `Detected project type in ${dir}: ${result.kind}`);
|
|
3513
3719
|
return result;
|
|
3514
3720
|
}
|
|
3515
3721
|
function detectUncached(dir) {
|
|
@@ -3603,13 +3809,13 @@ function hasNodeTestScript(dir) {
|
|
|
3603
3809
|
const pkg = JSON.parse(readFileSync3(`${dir}/package.json`, "utf-8"));
|
|
3604
3810
|
script = pkg.scripts?.test;
|
|
3605
3811
|
} catch (err) {
|
|
3606
|
-
log.warn(
|
|
3812
|
+
log.warn(TAG9, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
3607
3813
|
return false;
|
|
3608
3814
|
}
|
|
3609
3815
|
if (typeof script !== "string" || script.trim().length === 0)
|
|
3610
3816
|
return false;
|
|
3611
3817
|
if (NPM_PLACEHOLDER_TEST.test(script)) {
|
|
3612
|
-
log.info(
|
|
3818
|
+
log.info(TAG9, `package.json 'test' is the npm placeholder — skipping tests`);
|
|
3613
3819
|
return false;
|
|
3614
3820
|
}
|
|
3615
3821
|
return true;
|
|
@@ -3620,7 +3826,7 @@ function firstNodeScript(dir, candidates) {
|
|
|
3620
3826
|
const pkg = JSON.parse(readFileSync3(`${dir}/package.json`, "utf-8"));
|
|
3621
3827
|
scripts = pkg.scripts ?? {};
|
|
3622
3828
|
} catch (err) {
|
|
3623
|
-
log.warn(
|
|
3829
|
+
log.warn(TAG9, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
3624
3830
|
return null;
|
|
3625
3831
|
}
|
|
3626
3832
|
for (const name of candidates) {
|
|
@@ -3639,7 +3845,7 @@ function xcodeBuildCommand(pt) {
|
|
|
3639
3845
|
return null;
|
|
3640
3846
|
const scheme = resolveXcodeScheme(pt);
|
|
3641
3847
|
if (!scheme) {
|
|
3642
|
-
log.warn(
|
|
3848
|
+
log.warn(TAG9, "Could not resolve an Xcode scheme — skipping build (best-effort)");
|
|
3643
3849
|
return null;
|
|
3644
3850
|
}
|
|
3645
3851
|
const containerFlag = pt.xcodeIsWorkspace ? "-workspace" : "-project";
|
|
@@ -3667,7 +3873,7 @@ function resolveXcodeScheme(pt) {
|
|
|
3667
3873
|
const schemes = pt.xcodeIsWorkspace ? parsed.workspace?.schemes ?? [] : parsed.project?.schemes ?? [];
|
|
3668
3874
|
return schemes[0] ?? null;
|
|
3669
3875
|
} catch (err) {
|
|
3670
|
-
log.warn(
|
|
3876
|
+
log.warn(TAG9, `xcodebuild -list failed: ${err instanceof Error ? err.message : err}`);
|
|
3671
3877
|
return null;
|
|
3672
3878
|
}
|
|
3673
3879
|
}
|
|
@@ -3676,7 +3882,7 @@ function resolveXcodeScheme(pt) {
|
|
|
3676
3882
|
init_log();
|
|
3677
3883
|
init_run_containment();
|
|
3678
3884
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
3679
|
-
var
|
|
3885
|
+
var TAG10 = "revert-guard";
|
|
3680
3886
|
var TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
3681
3887
|
function isTestFile(path) {
|
|
3682
3888
|
return TEST_FILE.test(path);
|
|
@@ -3691,7 +3897,7 @@ function refetchBase(worktreePath, baseBranch) {
|
|
|
3691
3897
|
stdio: "pipe"
|
|
3692
3898
|
});
|
|
3693
3899
|
} catch {
|
|
3694
|
-
log.warn(
|
|
3900
|
+
log.warn(TAG10, "Failed to re-fetch base for revert guard — using last fetch");
|
|
3695
3901
|
}
|
|
3696
3902
|
}
|
|
3697
3903
|
function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
@@ -3706,7 +3912,7 @@ function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
|
3706
3912
|
return out.split(`
|
|
3707
3913
|
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
3708
3914
|
} catch (err) {
|
|
3709
|
-
log.warn(
|
|
3915
|
+
log.warn(TAG10, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
|
|
3710
3916
|
return [];
|
|
3711
3917
|
}
|
|
3712
3918
|
}
|
|
@@ -3717,8 +3923,8 @@ function findDeletedTestFiles(worktreePath, baseBranch) {
|
|
|
3717
3923
|
|
|
3718
3924
|
// src/verification.ts
|
|
3719
3925
|
init_run_containment();
|
|
3720
|
-
var
|
|
3721
|
-
var
|
|
3926
|
+
var TAG11 = "verification";
|
|
3927
|
+
var MAX_OUTPUT_BUFFER3 = 64 * 1024 * 1024;
|
|
3722
3928
|
async function runVerification(worktreePath, config, workerId) {
|
|
3723
3929
|
const result = {
|
|
3724
3930
|
passed: true,
|
|
@@ -3728,144 +3934,177 @@ async function runVerification(worktreePath, config, workerId) {
|
|
|
3728
3934
|
reviewFindings: [],
|
|
3729
3935
|
revertWarnings: []
|
|
3730
3936
|
};
|
|
3937
|
+
const sandbox = verificationSandbox(config);
|
|
3731
3938
|
if (config.verification.revertGuard) {
|
|
3732
|
-
log.info(
|
|
3939
|
+
log.info(TAG11, `[worker:${workerId}] Checking for reverted merged work...`);
|
|
3733
3940
|
const deletedTests = findDeletedTestFiles(worktreePath, config.worktree.baseBranch);
|
|
3734
3941
|
if (deletedTests.length > 0) {
|
|
3735
3942
|
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(
|
|
3943
|
+
log.warn(TAG11, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
|
|
3737
3944
|
result.passed = false;
|
|
3738
3945
|
} else {
|
|
3739
|
-
log.info(
|
|
3946
|
+
log.info(TAG11, `[worker:${workerId}] Revert guard passed`);
|
|
3740
3947
|
}
|
|
3741
3948
|
}
|
|
3742
3949
|
if (config.verification.build) {
|
|
3743
|
-
log.info(
|
|
3744
|
-
result.buildErrors = runBuild(worktreePath, config.verification.timeout);
|
|
3950
|
+
log.info(TAG11, `[worker:${workerId}] Running build...`);
|
|
3951
|
+
result.buildErrors = await runBuild(worktreePath, config.verification.timeout, sandbox);
|
|
3745
3952
|
if (result.buildErrors.length > 0) {
|
|
3746
|
-
log.warn(
|
|
3953
|
+
log.warn(TAG11, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
|
|
3747
3954
|
result.passed = false;
|
|
3748
3955
|
} else {
|
|
3749
|
-
log.info(
|
|
3956
|
+
log.info(TAG11, `[worker:${workerId}] Build passed`);
|
|
3750
3957
|
}
|
|
3751
3958
|
}
|
|
3752
3959
|
if (config.verification.test && result.buildErrors.length === 0) {
|
|
3753
|
-
log.info(
|
|
3754
|
-
result.testFailures = runTests(worktreePath, config.verification.testTimeout);
|
|
3960
|
+
log.info(TAG11, `[worker:${workerId}] Running tests...`);
|
|
3961
|
+
result.testFailures = await runTests(worktreePath, config.verification.testTimeout, sandbox);
|
|
3755
3962
|
if (result.testFailures.length > 0) {
|
|
3756
|
-
log.warn(
|
|
3963
|
+
log.warn(TAG11, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
|
|
3757
3964
|
result.passed = false;
|
|
3758
3965
|
} else {
|
|
3759
|
-
log.info(
|
|
3966
|
+
log.info(TAG11, `[worker:${workerId}] Tests passed`);
|
|
3760
3967
|
}
|
|
3761
3968
|
}
|
|
3762
3969
|
if (config.verification.lint) {
|
|
3763
|
-
log.info(
|
|
3764
|
-
result.lintWarnings = runLint(worktreePath, config.verification.timeout);
|
|
3970
|
+
log.info(TAG11, `[worker:${workerId}] Running lint...`);
|
|
3971
|
+
result.lintWarnings = await runLint(worktreePath, config.verification.timeout, sandbox);
|
|
3765
3972
|
if (result.lintWarnings.length > 0) {
|
|
3766
|
-
log.warn(
|
|
3973
|
+
log.warn(TAG11, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
|
|
3767
3974
|
} else {
|
|
3768
|
-
log.info(
|
|
3975
|
+
log.info(TAG11, `[worker:${workerId}] Lint passed`);
|
|
3769
3976
|
}
|
|
3770
3977
|
}
|
|
3771
3978
|
if (config.verification.deepReview) {
|
|
3772
|
-
log.info(
|
|
3979
|
+
log.info(TAG11, `[worker:${workerId}] Running deep review...`);
|
|
3773
3980
|
result.reviewFindings = await runDeepReview(worktreePath, config, workerId);
|
|
3774
3981
|
if (result.reviewFindings.length > 0) {
|
|
3775
|
-
log.warn(
|
|
3982
|
+
log.warn(TAG11, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
|
|
3776
3983
|
} else {
|
|
3777
|
-
log.info(
|
|
3984
|
+
log.info(TAG11, `[worker:${workerId}] Deep review passed`);
|
|
3778
3985
|
}
|
|
3779
3986
|
}
|
|
3780
3987
|
return result;
|
|
3781
3988
|
}
|
|
3782
|
-
function
|
|
3783
|
-
const
|
|
3784
|
-
if (!
|
|
3785
|
-
|
|
3786
|
-
|
|
3989
|
+
function verificationSandbox(config) {
|
|
3990
|
+
const image = config.verification.sandboxImage?.trim();
|
|
3991
|
+
if (!image)
|
|
3992
|
+
return;
|
|
3993
|
+
return { image };
|
|
3994
|
+
}
|
|
3995
|
+
async function execStep(command, args) {
|
|
3996
|
+
const { worktreePath, timeout } = args;
|
|
3997
|
+
const sandbox = args.sandbox?.image.trim() ? args.sandbox : undefined;
|
|
3998
|
+
if (sandbox) {
|
|
3999
|
+
if (!await sandboxAvailable()) {
|
|
4000
|
+
return {
|
|
4001
|
+
ok: false,
|
|
4002
|
+
sandboxError: `verification.sandboxImage is set to "${sandbox.image}" but no container runtime answered — ` + "start Docker or unset the image to verify on the host"
|
|
4003
|
+
};
|
|
4004
|
+
}
|
|
4005
|
+
const result = await runInSandbox({
|
|
4006
|
+
image: sandbox.image,
|
|
4007
|
+
worktree: worktreePath,
|
|
4008
|
+
command,
|
|
4009
|
+
timeoutMs: timeout + SANDBOX_STARTUP_GRACE_MS
|
|
4010
|
+
});
|
|
4011
|
+
if (result.sandboxError) {
|
|
4012
|
+
return { ok: false, sandboxError: result.sandboxError };
|
|
4013
|
+
}
|
|
4014
|
+
if (result.passed)
|
|
4015
|
+
return { ok: true };
|
|
4016
|
+
return { ok: false, err: { stdout: result.output, stderr: "" } };
|
|
3787
4017
|
}
|
|
3788
4018
|
try {
|
|
3789
4019
|
execFileSync5(command.cmd, command.args, {
|
|
3790
4020
|
cwd: worktreePath,
|
|
3791
4021
|
timeout,
|
|
3792
4022
|
stdio: "pipe",
|
|
3793
|
-
maxBuffer:
|
|
4023
|
+
maxBuffer: MAX_OUTPUT_BUFFER3,
|
|
3794
4024
|
env: containedEnv()
|
|
3795
4025
|
});
|
|
3796
|
-
return
|
|
4026
|
+
return { ok: true };
|
|
3797
4027
|
} catch (err) {
|
|
3798
|
-
return
|
|
4028
|
+
return { ok: false, err };
|
|
3799
4029
|
}
|
|
3800
4030
|
}
|
|
3801
|
-
function
|
|
4031
|
+
async function runBuild(worktreePath, timeout, sandbox) {
|
|
4032
|
+
const command = buildCommand(worktreePath);
|
|
4033
|
+
if (!command) {
|
|
4034
|
+
log.warn(TAG11, `No known build toolchain for ${worktreePath} — skipping build`);
|
|
4035
|
+
return [];
|
|
4036
|
+
}
|
|
4037
|
+
const outcome = await execStep(command, { worktreePath, timeout, sandbox });
|
|
4038
|
+
if (outcome.ok)
|
|
4039
|
+
return [];
|
|
4040
|
+
if (outcome.sandboxError) {
|
|
4041
|
+
log.error(TAG11, `Build not verified: ${outcome.sandboxError}`);
|
|
4042
|
+
return [`Build did not run: ${outcome.sandboxError}`];
|
|
4043
|
+
}
|
|
4044
|
+
return parseErrorOutput(outcome.err);
|
|
4045
|
+
}
|
|
4046
|
+
async function runTests(worktreePath, timeout, sandbox) {
|
|
3802
4047
|
const command = testCommand(worktreePath);
|
|
3803
4048
|
if (!command) {
|
|
3804
|
-
log.warn(
|
|
4049
|
+
log.warn(TAG11, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
|
|
3805
4050
|
return [];
|
|
3806
4051
|
}
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
cwd: worktreePath,
|
|
3810
|
-
timeout,
|
|
3811
|
-
stdio: "pipe",
|
|
3812
|
-
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
3813
|
-
env: containedEnv()
|
|
3814
|
-
});
|
|
4052
|
+
const outcome = await execStep(command, { worktreePath, timeout, sandbox });
|
|
4053
|
+
if (outcome.ok)
|
|
3815
4054
|
return [];
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
${output.slice(-4000) || "(no output captured)"}`);
|
|
3820
|
-
return parseTestFailures(err, timeout);
|
|
4055
|
+
if (outcome.sandboxError) {
|
|
4056
|
+
log.error(TAG11, `Tests not verified: ${outcome.sandboxError}`);
|
|
4057
|
+
return [`Test run did not happen: ${outcome.sandboxError}`];
|
|
3821
4058
|
}
|
|
4059
|
+
const output = combineOutput(outcome.err);
|
|
4060
|
+
log.warn(TAG11, `Test run failed:
|
|
4061
|
+
${output.slice(-4000) || "(no output captured)"}`);
|
|
4062
|
+
return parseTestFailures(outcome.err, timeout);
|
|
3822
4063
|
}
|
|
3823
|
-
function runFormatFix(worktreePath, timeout, workerId) {
|
|
4064
|
+
async function runFormatFix(worktreePath, timeout, workerId, sandbox) {
|
|
3824
4065
|
const command = formatFixCommand(worktreePath);
|
|
3825
4066
|
if (!command)
|
|
3826
4067
|
return;
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
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)}`);
|
|
4068
|
+
const outcome = await execStep(command, { worktreePath, timeout, sandbox });
|
|
4069
|
+
if (outcome.ok) {
|
|
4070
|
+
log.info(TAG11, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
|
|
4071
|
+
return;
|
|
3838
4072
|
}
|
|
4073
|
+
const why = outcome.sandboxError ? outcome.sandboxError : outcome.err instanceof Error ? outcome.err.message : String(outcome.err);
|
|
4074
|
+
log.warn(TAG11, `[worker:${workerId}] Auto-format step did not complete (non-fatal): ${why}`);
|
|
3839
4075
|
}
|
|
3840
|
-
function runLint(worktreePath, timeout) {
|
|
4076
|
+
async function runLint(worktreePath, timeout, sandbox) {
|
|
3841
4077
|
const command = lintCommand(worktreePath);
|
|
3842
4078
|
if (!command) {
|
|
3843
|
-
log.info(
|
|
4079
|
+
log.info(TAG11, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
|
|
3844
4080
|
return [];
|
|
3845
4081
|
}
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
cwd: worktreePath,
|
|
3849
|
-
timeout,
|
|
3850
|
-
stdio: "pipe",
|
|
3851
|
-
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
3852
|
-
env: containedEnv()
|
|
3853
|
-
});
|
|
4082
|
+
const outcome = await execStep(command, { worktreePath, timeout, sandbox });
|
|
4083
|
+
if (outcome.ok)
|
|
3854
4084
|
return [];
|
|
3855
|
-
|
|
3856
|
-
|
|
4085
|
+
if (outcome.sandboxError) {
|
|
4086
|
+
log.error(TAG11, `Lint not verified: ${outcome.sandboxError}`);
|
|
4087
|
+
return [`Lint did not run: ${outcome.sandboxError}`];
|
|
3857
4088
|
}
|
|
4089
|
+
return parseErrorOutput(outcome.err);
|
|
3858
4090
|
}
|
|
3859
4091
|
async function runDeepReview(worktreePath, config, workerId) {
|
|
3860
4092
|
if (!supportsDevServer(worktreePath)) {
|
|
3861
|
-
log.info(
|
|
4093
|
+
log.info(TAG11, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
|
|
3862
4094
|
return [];
|
|
3863
4095
|
}
|
|
3864
4096
|
const port = config.verification.devServerBasePort + workerId;
|
|
3865
4097
|
let devServer = null;
|
|
4098
|
+
const launch = devServerLaunch({
|
|
4099
|
+
worktreePath,
|
|
4100
|
+
port,
|
|
4101
|
+
sandbox: verificationSandbox(config)
|
|
4102
|
+
});
|
|
3866
4103
|
try {
|
|
3867
|
-
|
|
3868
|
-
|
|
4104
|
+
if (launch.containerName) {
|
|
4105
|
+
await removeSandboxContainer(launch.containerName);
|
|
4106
|
+
}
|
|
4107
|
+
devServer = spawn2(launch.cmd, launch.args, {
|
|
3869
4108
|
cwd: worktreePath,
|
|
3870
4109
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3871
4110
|
env: containedEnv()
|
|
@@ -3874,7 +4113,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
3874
4113
|
await waitForDevServer(devServer, 30000);
|
|
3875
4114
|
await probeDevServer(port);
|
|
3876
4115
|
} catch (err) {
|
|
3877
|
-
log.error(
|
|
4116
|
+
log.error(TAG11, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
|
|
3878
4117
|
return [];
|
|
3879
4118
|
}
|
|
3880
4119
|
let diff = "";
|
|
@@ -3883,7 +4122,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
3883
4122
|
cwd: worktreePath,
|
|
3884
4123
|
encoding: "utf-8",
|
|
3885
4124
|
timeout: 30000,
|
|
3886
|
-
maxBuffer:
|
|
4125
|
+
maxBuffer: MAX_OUTPUT_BUFFER3
|
|
3887
4126
|
});
|
|
3888
4127
|
} catch {
|
|
3889
4128
|
diff = "(unable to retrieve diff)";
|
|
@@ -3917,17 +4156,20 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
3917
4156
|
encoding: "utf-8",
|
|
3918
4157
|
timeout: config.verification.timeout,
|
|
3919
4158
|
stdio: "pipe",
|
|
3920
|
-
maxBuffer:
|
|
4159
|
+
maxBuffer: MAX_OUTPUT_BUFFER3,
|
|
3921
4160
|
env: containedEnv()
|
|
3922
4161
|
});
|
|
3923
4162
|
return parseReviewFindings(output);
|
|
3924
4163
|
} catch (err) {
|
|
3925
|
-
log.error(
|
|
4164
|
+
log.error(TAG11, `Deep review failed: ${err instanceof Error ? err.message : err}`);
|
|
3926
4165
|
return [];
|
|
3927
4166
|
} finally {
|
|
3928
4167
|
if (devServer && !devServer.killed) {
|
|
3929
4168
|
devServer.kill("SIGTERM");
|
|
3930
4169
|
}
|
|
4170
|
+
if (launch.containerName) {
|
|
4171
|
+
await removeSandboxContainer(launch.containerName);
|
|
4172
|
+
}
|
|
3931
4173
|
}
|
|
3932
4174
|
}
|
|
3933
4175
|
function attemptAutoFix(worktreePath, config, errors) {
|
|
@@ -3960,12 +4202,12 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
3960
4202
|
"--",
|
|
3961
4203
|
fixPrompt
|
|
3962
4204
|
];
|
|
3963
|
-
log.info(
|
|
4205
|
+
log.info(TAG11, "Spawning Claude for auto-fix...");
|
|
3964
4206
|
execFileSync5("claude", args, {
|
|
3965
4207
|
cwd: worktreePath,
|
|
3966
4208
|
timeout: config.verification.timeout,
|
|
3967
4209
|
stdio: "pipe",
|
|
3968
|
-
maxBuffer:
|
|
4210
|
+
maxBuffer: MAX_OUTPUT_BUFFER3,
|
|
3969
4211
|
env: containedEnv()
|
|
3970
4212
|
});
|
|
3971
4213
|
}
|
|
@@ -3999,7 +4241,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
3999
4241
|
try {
|
|
4000
4242
|
await client.createSubtask(cardId, title);
|
|
4001
4243
|
} catch (err) {
|
|
4002
|
-
log.error(
|
|
4244
|
+
log.error(TAG11, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
|
|
4003
4245
|
}
|
|
4004
4246
|
}));
|
|
4005
4247
|
if (overflow > 0) {
|
|
@@ -4007,7 +4249,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
4007
4249
|
await client.createSubtask(cardId, `...and ${overflow} more issues`);
|
|
4008
4250
|
} catch {}
|
|
4009
4251
|
}
|
|
4010
|
-
log.info(
|
|
4252
|
+
log.info(TAG11, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
|
|
4011
4253
|
}
|
|
4012
4254
|
function combineOutput(err) {
|
|
4013
4255
|
const stderr = err?.stderr?.toString() ?? "";
|
|
@@ -4040,7 +4282,7 @@ function parseTestFailures(err, timeout) {
|
|
|
4040
4282
|
}
|
|
4041
4283
|
if (e?.code === "ENOBUFS") {
|
|
4042
4284
|
return [
|
|
4043
|
-
`Test output exceeded the ${
|
|
4285
|
+
`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
4286
|
];
|
|
4045
4287
|
}
|
|
4046
4288
|
const combined = combineOutput(err);
|
|
@@ -4066,6 +4308,24 @@ class DevServerReadinessError extends Error {
|
|
|
4066
4308
|
this.name = "DevServerReadinessError";
|
|
4067
4309
|
}
|
|
4068
4310
|
}
|
|
4311
|
+
function devServerLaunch(args) {
|
|
4312
|
+
const [cmd, runArgs] = spawnRunArgs("dev", "--port", String(args.port), ...args.sandbox ? ["--host", SANDBOX_DEV_SERVER_BIND] : []);
|
|
4313
|
+
if (!args.sandbox)
|
|
4314
|
+
return { cmd, args: runArgs };
|
|
4315
|
+
const containerName = devServerContainerName(args.port);
|
|
4316
|
+
return {
|
|
4317
|
+
cmd: "docker",
|
|
4318
|
+
args: devServerSandboxArgs({
|
|
4319
|
+
image: args.sandbox.image,
|
|
4320
|
+
worktree: args.worktreePath,
|
|
4321
|
+
command: { cmd, args: runArgs },
|
|
4322
|
+
port: args.port,
|
|
4323
|
+
name: containerName
|
|
4324
|
+
}),
|
|
4325
|
+
containerName
|
|
4326
|
+
};
|
|
4327
|
+
}
|
|
4328
|
+
var DEV_SERVER_READY = /\bready\b/i;
|
|
4069
4329
|
function waitForDevServer(proc, timeout) {
|
|
4070
4330
|
return new Promise((resolve3, reject) => {
|
|
4071
4331
|
let settled = false;
|
|
@@ -4095,7 +4355,7 @@ function waitForDevServer(proc, timeout) {
|
|
|
4095
4355
|
}, timeout);
|
|
4096
4356
|
const onData = (data) => {
|
|
4097
4357
|
const text = data.toString();
|
|
4098
|
-
if (
|
|
4358
|
+
if (DEV_SERVER_READY.test(text) || text.includes("localhost") || text.includes("Local:")) {
|
|
4099
4359
|
settleResolve();
|
|
4100
4360
|
}
|
|
4101
4361
|
};
|
|
@@ -4131,7 +4391,7 @@ async function probeDevServer(port, timeoutMs = 5000) {
|
|
|
4131
4391
|
}
|
|
4132
4392
|
|
|
4133
4393
|
// src/gate-collectors.ts
|
|
4134
|
-
var
|
|
4394
|
+
var TAG12 = "gate-collectors";
|
|
4135
4395
|
async function resolveStageGate(client, card) {
|
|
4136
4396
|
const currentStage = card.current_stage;
|
|
4137
4397
|
const playbookId = card.playbook_id;
|
|
@@ -4149,7 +4409,7 @@ async function resolveStageGate(client, card) {
|
|
|
4149
4409
|
return null;
|
|
4150
4410
|
return { stage: resolution.stage, gate };
|
|
4151
4411
|
} catch (err) {
|
|
4152
|
-
log.warn(
|
|
4412
|
+
log.warn(TAG12, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
|
|
4153
4413
|
return null;
|
|
4154
4414
|
}
|
|
4155
4415
|
}
|
|
@@ -4184,8 +4444,8 @@ class BuildGreenCollector {
|
|
|
4184
4444
|
async collect(_context) {
|
|
4185
4445
|
const doBuild = this.deps.runBuild ?? runBuild;
|
|
4186
4446
|
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);
|
|
4447
|
+
const buildErrors = await doBuild(this.deps.worktreePath, this.deps.buildTimeout, this.deps.sandbox);
|
|
4448
|
+
const lintWarnings = await doLint(this.deps.worktreePath, this.deps.lintTimeout, this.deps.sandbox);
|
|
4189
4449
|
const buildPassed = buildErrors.length === 0;
|
|
4190
4450
|
const lintPassed = lintWarnings.length === 0;
|
|
4191
4451
|
const result = buildPassed ? "passed" : "failed";
|
|
@@ -4281,7 +4541,7 @@ function buildGateCollectorRegistry(deps) {
|
|
|
4281
4541
|
async function collectGateEvidence(registry, context) {
|
|
4282
4542
|
const collector = registry[context.gate.kind];
|
|
4283
4543
|
if (!collector) {
|
|
4284
|
-
log.info(
|
|
4544
|
+
log.info(TAG12, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
|
|
4285
4545
|
return {
|
|
4286
4546
|
result: "blocked",
|
|
4287
4547
|
structured: {
|
|
@@ -4293,7 +4553,7 @@ async function collectGateEvidence(registry, context) {
|
|
|
4293
4553
|
return await collector.collect(context);
|
|
4294
4554
|
} catch (err) {
|
|
4295
4555
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4296
|
-
log.warn(
|
|
4556
|
+
log.warn(TAG12, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
|
|
4297
4557
|
return { result: "blocked", structured: { error: msg } };
|
|
4298
4558
|
}
|
|
4299
4559
|
}
|
|
@@ -4301,7 +4561,7 @@ async function collectGateEvidence(registry, context) {
|
|
|
4301
4561
|
init_log();
|
|
4302
4562
|
init_run_containment();
|
|
4303
4563
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
4304
|
-
var
|
|
4564
|
+
var TAG13 = "git-diff-stat";
|
|
4305
4565
|
var MAX_CHANGED_FILES = 30;
|
|
4306
4566
|
function parseNumstat(raw, maxFiles = MAX_CHANGED_FILES) {
|
|
4307
4567
|
const files = [];
|
|
@@ -4383,7 +4643,7 @@ function captureDiffStat(worktreePath, baseBranch, maxFiles = MAX_CHANGED_FILES)
|
|
|
4383
4643
|
const raw = execFileSync6("git", [...GIT_NO_HOOKS, "diff", "--numstat", `${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
|
|
4384
4644
|
return parseNumstat(raw, maxFiles);
|
|
4385
4645
|
} catch (err) {
|
|
4386
|
-
log.warn(
|
|
4646
|
+
log.warn(TAG13, "git diff --numstat failed", {
|
|
4387
4647
|
event: "diff_stat_failed",
|
|
4388
4648
|
error: err instanceof Error ? err.message : String(err)
|
|
4389
4649
|
});
|
|
@@ -4396,7 +4656,7 @@ init_git_pr();
|
|
|
4396
4656
|
|
|
4397
4657
|
// src/harmony-client.ts
|
|
4398
4658
|
init_log();
|
|
4399
|
-
var
|
|
4659
|
+
var TAG15 = "harmony-client";
|
|
4400
4660
|
function readClientConfig(env) {
|
|
4401
4661
|
const apiUrl = env.HARMONY_API_URL?.trim();
|
|
4402
4662
|
const apiKey = env.HARMONY_API_KEY?.trim();
|
|
@@ -4448,7 +4708,7 @@ class HarmonyClient {
|
|
|
4448
4708
|
purpose: "gate_evaluation"
|
|
4449
4709
|
});
|
|
4450
4710
|
if (!response.ok) {
|
|
4451
|
-
log.warn(
|
|
4711
|
+
log.warn(TAG15, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
|
|
4452
4712
|
return null;
|
|
4453
4713
|
}
|
|
4454
4714
|
const body = await response.json();
|
|
@@ -4528,102 +4788,6 @@ function relayAgentEvent(draft) {
|
|
|
4528
4788
|
}
|
|
4529
4789
|
return { type: "agent_event", event: draft };
|
|
4530
4790
|
}
|
|
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
4791
|
|
|
4628
4792
|
// src/index.ts
|
|
4629
4793
|
init_run_containment();
|
|
@@ -5153,7 +5317,7 @@ function branchAheadOfItsRemote(branchName, repoRoot = resolveRepoRoot()) {
|
|
|
5153
5317
|
return false;
|
|
5154
5318
|
}
|
|
5155
5319
|
}
|
|
5156
|
-
async function rescueUnpushedBranch(client, cardId, branchName, repoRoot = resolveRepoRoot()) {
|
|
5320
|
+
async function rescueUnpushedBranch(client, cardId, branchName, repoRoot = resolveRepoRoot(), agentSessionId) {
|
|
5157
5321
|
const { getBranchWebUrl: getBranchWebUrl2, pushBranch: pushBranch2 } = await Promise.resolve().then(() => (init_git_pr(), exports_git_pr));
|
|
5158
5322
|
try {
|
|
5159
5323
|
pushBranch2(branchName, repoRoot);
|
|
@@ -5166,13 +5330,16 @@ async function rescueUnpushedBranch(client, cardId, branchName, repoRoot = resol
|
|
|
5166
5330
|
const url = getBranchWebUrl2(branchName, repoRoot);
|
|
5167
5331
|
const recover = url ? `View it at ${url} or recover locally: \`git fetch && git checkout ${branchName}\`` : `Recover it locally: \`git fetch && git checkout ${branchName}\``;
|
|
5168
5332
|
const body = `⚠ Run ended before completion. Committed work was push-rescued to ` + `\`origin/${branchName}\` so it isn't lost. ${recover}`;
|
|
5169
|
-
await client.addComment(cardId, body, {
|
|
5333
|
+
await client.addComment(cardId, body, {
|
|
5334
|
+
commentType: "message",
|
|
5335
|
+
...agentSessionId ? { agentSessionId } : {}
|
|
5336
|
+
});
|
|
5170
5337
|
} catch (err) {
|
|
5171
5338
|
log.warn(TAG16, `push-rescue comment failed for ${branchName} (work is still safe on origin): ${err instanceof Error ? err.message : err}`);
|
|
5172
5339
|
}
|
|
5173
5340
|
return true;
|
|
5174
5341
|
}
|
|
5175
|
-
async function teardownWorktree(client, cardId, worktreePath, branchName) {
|
|
5342
|
+
async function teardownWorktree(client, cardId, worktreePath, branchName, agentSessionId) {
|
|
5176
5343
|
let skipBranchDelete = false;
|
|
5177
5344
|
if (branchName && cardId) {
|
|
5178
5345
|
let repoRoot;
|
|
@@ -5183,7 +5350,7 @@ async function teardownWorktree(client, cardId, worktreePath, branchName) {
|
|
|
5183
5350
|
return;
|
|
5184
5351
|
}
|
|
5185
5352
|
if (branchAheadOfItsRemote(branchName, repoRoot)) {
|
|
5186
|
-
const ok = await rescueUnpushedBranch(client, cardId, branchName, repoRoot);
|
|
5353
|
+
const ok = await rescueUnpushedBranch(client, cardId, branchName, repoRoot, agentSessionId);
|
|
5187
5354
|
if (!ok) {
|
|
5188
5355
|
skipBranchDelete = true;
|
|
5189
5356
|
log.error(TAG16, `Keeping local branch ${branchName} (push-rescue failed) to avoid orphaning its commit`);
|
|
@@ -5202,6 +5369,7 @@ var MOTOR_NAME = "harmony-harness";
|
|
|
5202
5369
|
export {
|
|
5203
5370
|
writeOnlyDenyPaths,
|
|
5204
5371
|
waitForDevServer,
|
|
5372
|
+
verificationSandbox,
|
|
5205
5373
|
validateGitProviderCli,
|
|
5206
5374
|
upsertReviewedSha,
|
|
5207
5375
|
upsertCiReviewRequestedSha,
|
|
@@ -5247,6 +5415,7 @@ export {
|
|
|
5247
5415
|
reportFindings,
|
|
5248
5416
|
renameRemoteBranch,
|
|
5249
5417
|
removeWorktreeHoldingBranch,
|
|
5418
|
+
removeSandboxContainer,
|
|
5250
5419
|
remove,
|
|
5251
5420
|
remoteBranchExists,
|
|
5252
5421
|
relayAgentEvent,
|
|
@@ -5278,6 +5447,8 @@ export {
|
|
|
5278
5447
|
implementRunContainmentCliArgs,
|
|
5279
5448
|
implementRunContainment,
|
|
5280
5449
|
hostPersistencePaths,
|
|
5450
|
+
heldTestEnvKeysToStrip,
|
|
5451
|
+
heldOracleSandboxArgv,
|
|
5281
5452
|
harmonyMcpServer,
|
|
5282
5453
|
gradeOracleRed,
|
|
5283
5454
|
gitMetadataDenyPaths,
|
|
@@ -5300,6 +5471,9 @@ export {
|
|
|
5300
5471
|
extractCiReviewRequestedSha,
|
|
5301
5472
|
extractAzurePrId,
|
|
5302
5473
|
envKeysDroppedByLaunch,
|
|
5474
|
+
devServerSandboxArgs,
|
|
5475
|
+
devServerLaunch,
|
|
5476
|
+
devServerContainerName,
|
|
5303
5477
|
detectPackageManager,
|
|
5304
5478
|
detectGitProvider,
|
|
5305
5479
|
detect,
|
|
@@ -5343,9 +5517,12 @@ export {
|
|
|
5343
5517
|
SdkAgentRunner,
|
|
5344
5518
|
SIZING_MODEL,
|
|
5345
5519
|
SDK_ALLOWED_TOOLS,
|
|
5520
|
+
SANDBOX_STARTUP_GRACE_MS,
|
|
5346
5521
|
SANDBOX_MOUNT,
|
|
5522
|
+
SANDBOX_DEV_SERVER_BIND,
|
|
5347
5523
|
ReviewPassedCollector,
|
|
5348
5524
|
ProjectSandboxOverrideError,
|
|
5525
|
+
OracleSandboxConfigError,
|
|
5349
5526
|
OracleRedCollector,
|
|
5350
5527
|
OracleCollector,
|
|
5351
5528
|
ORACLE_RUNNER_HINTS,
|