@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/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, SWEPT_SESSION_WRITE_GRACE_MS, ACTIVE_STATUSES;
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 promisify2 } from "node:util";
1146
+ import { promisify as promisify3 } from "node:util";
1132
1147
  function createExecFileAsync2() {
1133
- return promisify2(execFile2);
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(TAG13, `Git provider "${provider}" — PR creation will be skipped (no CLI support)`);
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(TAG13, `Failed to parse glab JSON output for MR ${mrMatch[1]}`);
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(TAG13, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
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(TAG13, `az repos pr show failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
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(TAG13, `Remote branch ${branchName} exists (rework), force-pushing`);
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(TAG13, `could not resolve remote tip for ${branchName}, falling back to weak lease: ${err instanceof Error ? err.message : err}`);
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(TAG13, `Renaming remote ${oldRef} → ${newRef}`);
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(TAG13, `renameRemoteBranch: could not delete old ref ${oldRef}: ${err instanceof Error ? err.message : err}`);
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(TAG13, `Reusing existing PR from card description: ${existingPrUrl}`);
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(TAG13, `PR already exists for ${branchName}, updating body...`);
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(TAG13, `No PR CLI for provider "${provider}" — branch pushed but no PR created`);
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(TAG13, `PR created: ${result}`);
1824
+ log.info(TAG14, `PR created: ${result}`);
1810
1825
  return result;
1811
1826
  } catch (err) {
1812
- log.error(TAG13, `Failed to create PR: ${err instanceof Error ? err.message : err}`);
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(TAG13, `Updated existing PR body for ${branchName}`);
1861
+ log.info(TAG14, `Updated existing PR body for ${branchName}`);
1847
1862
  } catch (err) {
1848
- log.warn(TAG13, `Failed to update PR body: ${err instanceof Error ? err.message : err}`);
1863
+ log.warn(TAG14, `Failed to update PR body: ${err instanceof Error ? err.message : err}`);
1849
1864
  }
1850
1865
  }
1851
- var cachedExecFileAsync2, TAG13 = "git-pr", VALID_PR_URL_RE, PR_URL_RE, REVIEWED_SHA_RE, CI_REVIEW_REQUESTED_SHA_RE;
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 { lstatSync, readFileSync as readFileSync2, statSync } from "node:fs";
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
- var TAG5 = "oracle";
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, timeoutMs = DEFAULT_METRIC_TIMEOUT_MS) {
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(spec, repoPath, oracle, reportPath, timeoutMs);
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(TAG5, `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.`);
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
- async function spawnHeldOracle(spec, repoPath, oracle, reportPath, timeoutMs) {
3171
- const { command, args: baseArgs } = spec.argv(argvPath(oracle.path));
3172
- const args = [...baseArgs, ...spec.report.flags(reportPath)];
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
- report = spec.report.parse(readFileSync2(reportPath, "utf8"));
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(TAG5, `No oracle report at ${reportPath} — no verdict.`);
3417
+ log.info(TAG6, `No oracle report at ${reportPath} — no verdict.`);
3220
3418
  } else {
3221
- log.warn(TAG5, `Refusing the oracle report at ${reportPath}: ${message} — the gate will report no verdict.`);
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 TAG6 = "oracle-collector";
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(TAG6, `${reason} — blocked (config)`);
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(TAG6, `No oracle held for stage ${stageId} — blocked`);
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(TAG6, logLine);
3543
+ log.info(TAG7, logLine);
3340
3544
  } else {
3341
- log.warn(TAG6, logLine);
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
- log.warn(TAG6, `Oracle run threw: ${message} — blocked`);
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(TAG6, `Removing the held test at ${oracle.path} failed (${errText(err)}) — retrying once`);
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(TAG6, `Held test at ${oracle.path} removed on the second attempt`);
3579
+ log.info(TAG7, `Held test at ${oracle.path} removed on the second attempt`);
3374
3580
  } catch (err) {
3375
- log.error(TAG6, `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.");
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(TAG6, `Red gate for ${oracle.path}: ${graded.reason} — blocked`);
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 TAG7 = "pm";
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(TAG7, `Detected package manager: ${cached}`);
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 TAG8 = "project-type";
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(TAG8, `Detected project type in ${dir}: ${result.kind}`);
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(TAG8, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
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(TAG8, `package.json 'test' is the npm placeholder — skipping tests`);
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(TAG8, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
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(TAG8, "Could not resolve an Xcode scheme — skipping build (best-effort)");
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(TAG8, `xcodebuild -list failed: ${err instanceof Error ? err.message : err}`);
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 TAG9 = "revert-guard";
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(TAG9, "Failed to re-fetch base for revert guard — using last fetch");
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(TAG9, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
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 TAG10 = "verification";
3721
- var MAX_OUTPUT_BUFFER2 = 64 * 1024 * 1024;
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(TAG10, `[worker:${workerId}] Checking for reverted merged work...`);
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(TAG10, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
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(TAG10, `[worker:${workerId}] Revert guard passed`);
3946
+ log.info(TAG11, `[worker:${workerId}] Revert guard passed`);
3740
3947
  }
3741
3948
  }
3742
3949
  if (config.verification.build) {
3743
- log.info(TAG10, `[worker:${workerId}] Running build...`);
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(TAG10, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
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(TAG10, `[worker:${workerId}] Build passed`);
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(TAG10, `[worker:${workerId}] Running tests...`);
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(TAG10, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
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(TAG10, `[worker:${workerId}] Tests passed`);
3966
+ log.info(TAG11, `[worker:${workerId}] Tests passed`);
3760
3967
  }
3761
3968
  }
3762
3969
  if (config.verification.lint) {
3763
- log.info(TAG10, `[worker:${workerId}] Running lint...`);
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(TAG10, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
3973
+ log.warn(TAG11, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
3767
3974
  } else {
3768
- log.info(TAG10, `[worker:${workerId}] Lint passed`);
3975
+ log.info(TAG11, `[worker:${workerId}] Lint passed`);
3769
3976
  }
3770
3977
  }
3771
3978
  if (config.verification.deepReview) {
3772
- log.info(TAG10, `[worker:${workerId}] Running deep review...`);
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(TAG10, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
3982
+ log.warn(TAG11, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
3776
3983
  } else {
3777
- log.info(TAG10, `[worker:${workerId}] Deep review passed`);
3984
+ log.info(TAG11, `[worker:${workerId}] Deep review passed`);
3778
3985
  }
3779
3986
  }
3780
3987
  return result;
3781
3988
  }
3782
- function runBuild(worktreePath, timeout) {
3783
- const command = buildCommand(worktreePath);
3784
- if (!command) {
3785
- log.warn(TAG10, `No known build toolchain for ${worktreePath} — skipping build`);
3786
- return [];
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: MAX_OUTPUT_BUFFER2,
4023
+ maxBuffer: MAX_OUTPUT_BUFFER3,
3794
4024
  env: containedEnv()
3795
4025
  });
3796
- return [];
4026
+ return { ok: true };
3797
4027
  } catch (err) {
3798
- return parseErrorOutput(err);
4028
+ return { ok: false, err };
3799
4029
  }
3800
4030
  }
3801
- function runTests(worktreePath, timeout) {
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(TAG10, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
4049
+ log.warn(TAG11, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
3805
4050
  return [];
3806
4051
  }
3807
- try {
3808
- execFileSync5(command.cmd, command.args, {
3809
- cwd: worktreePath,
3810
- timeout,
3811
- stdio: "pipe",
3812
- maxBuffer: MAX_OUTPUT_BUFFER2,
3813
- env: containedEnv()
3814
- });
4052
+ const outcome = await execStep(command, { worktreePath, timeout, sandbox });
4053
+ if (outcome.ok)
3815
4054
  return [];
3816
- } catch (err) {
3817
- const output = combineOutput(err);
3818
- log.warn(TAG10, `Test run failed:
3819
- ${output.slice(-4000) || "(no output captured)"}`);
3820
- return parseTestFailures(err, timeout);
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
- try {
3828
- execFileSync5(command.cmd, command.args, {
3829
- cwd: worktreePath,
3830
- timeout,
3831
- stdio: "pipe",
3832
- maxBuffer: MAX_OUTPUT_BUFFER2,
3833
- env: containedEnv()
3834
- });
3835
- log.info(TAG10, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
3836
- } catch (err) {
3837
- log.warn(TAG10, `[worker:${workerId}] Auto-format step exited non-zero (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
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(TAG10, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
4079
+ log.info(TAG11, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
3844
4080
  return [];
3845
4081
  }
3846
- try {
3847
- execFileSync5(command.cmd, command.args, {
3848
- cwd: worktreePath,
3849
- timeout,
3850
- stdio: "pipe",
3851
- maxBuffer: MAX_OUTPUT_BUFFER2,
3852
- env: containedEnv()
3853
- });
4082
+ const outcome = await execStep(command, { worktreePath, timeout, sandbox });
4083
+ if (outcome.ok)
3854
4084
  return [];
3855
- } catch (err) {
3856
- return parseErrorOutput(err);
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(TAG10, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
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
- const [cmd, args] = spawnRunArgs("dev", "--port", String(port));
3868
- devServer = spawn2(cmd, args, {
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(TAG10, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
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: MAX_OUTPUT_BUFFER2
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: MAX_OUTPUT_BUFFER2,
4159
+ maxBuffer: MAX_OUTPUT_BUFFER3,
3921
4160
  env: containedEnv()
3922
4161
  });
3923
4162
  return parseReviewFindings(output);
3924
4163
  } catch (err) {
3925
- log.error(TAG10, `Deep review failed: ${err instanceof Error ? err.message : err}`);
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(TAG10, "Spawning Claude for auto-fix...");
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: MAX_OUTPUT_BUFFER2,
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(TAG10, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
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(TAG10, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
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 ${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."
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 (text.includes("ready") || text.includes("localhost") || text.includes("Local:")) {
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 TAG11 = "gate-collectors";
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(TAG11, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
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(TAG11, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
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(TAG11, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
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 TAG12 = "git-diff-stat";
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(TAG12, "git diff --numstat failed", {
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 TAG14 = "harmony-client";
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(TAG14, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
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, { commentType: "message" });
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,