@gethmy/harness 1.6.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"],
@@ -2989,7 +3004,8 @@ import { createHash as createHash2 } from "node:crypto";
2989
3004
  init_log();
2990
3005
 
2991
3006
  // src/oracle.ts
2992
- 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";
2993
3009
  import {
2994
3010
  chmod,
2995
3011
  lstat,
@@ -3003,7 +3019,140 @@ import { tmpdir as tmpdir2 } from "node:os";
3003
3019
  import { dirname as dirname3, isAbsolute as isAbsolute3, join as join2, resolve as resolve2, sep as sep2 } from "node:path";
3004
3020
  import { StringDecoder } from "node:string_decoder";
3005
3021
  init_log();
3006
- 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
+ }
3007
3156
  async function resolveContained(repoPath, relativePath) {
3008
3157
  if (isAbsolute3(relativePath)) {
3009
3158
  throw new Error(`refusing to place an oracle at an absolute path: ${relativePath}`);
@@ -3134,12 +3283,24 @@ function gradeOracleRed(summary, exitCode) {
3134
3283
  function argvPath(path) {
3135
3284
  return path.startsWith("./") ? path : `./${path}`;
3136
3285
  }
3137
- 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
+ }
3138
3292
  const spec = resolveOracleRunnerSpec(oracle);
3139
3293
  const reportDir = await mkdtemp(join2(tmpdir2(), reportDirPrefix()));
3140
3294
  const reportPath = join2(reportDir, spec.report.file);
3141
3295
  try {
3142
- 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
+ });
3143
3304
  } finally {
3144
3305
  await removeReportDir(reportDir);
3145
3306
  }
@@ -3160,23 +3321,57 @@ function assertUntampered(reportPath) {
3160
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`);
3161
3322
  }
3162
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
+ }
3163
3331
  async function removeReportDir(reportDir) {
3164
3332
  try {
3165
3333
  await chmod(reportDir, 448).catch(() => {});
3166
3334
  await rm(reportDir, { recursive: true, force: true });
3167
3335
  } catch (err) {
3168
- 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.`);
3169
3337
  }
3170
3338
  }
3171
- async function spawnHeldOracle(spec, repoPath, oracle, reportPath, timeoutMs) {
3172
- const { command, args: baseArgs } = spec.argv(argvPath(oracle.path));
3173
- 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
+ };
3174
3368
  return await new Promise((settleOk, settleErr) => {
3175
3369
  let child;
3176
3370
  try {
3177
3371
  child = spawnInGroup(command, args, {
3178
3372
  cwd: repoPath,
3179
- stdio: ["ignore", "pipe", "pipe"]
3373
+ stdio: ["ignore", "pipe", "pipe"],
3374
+ stripEnvKeys: heldTestEnvKeysToStrip()
3180
3375
  });
3181
3376
  } catch (err) {
3182
3377
  settleErr(err);
@@ -3208,18 +3403,20 @@ ${output}` : output;
3208
3403
  return verdict;
3209
3404
  };
3210
3405
  let report = null;
3211
- const captureReport = () => {
3406
+ const captureReport = (exitCode) => {
3212
3407
  try {
3213
3408
  assertUntampered(reportPath);
3214
- report = spec.report.parse(readFileSync2(reportPath, "utf8"));
3409
+ const parsed = spec.report.parse(readFileSync2(reportPath, "utf8"));
3410
+ assertConsistentWithExit(parsed, exitCode);
3411
+ report = parsed;
3215
3412
  } catch (err) {
3216
3413
  report = null;
3217
3414
  const message = err instanceof Error ? err.message : String(err);
3218
3415
  const absent = err instanceof Error && err.code === "ENOENT";
3219
3416
  if (absent) {
3220
- log.info(TAG5, `No oracle report at ${reportPath} — no verdict.`);
3417
+ log.info(TAG6, `No oracle report at ${reportPath} — no verdict.`);
3221
3418
  } else {
3222
- 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.`);
3223
3420
  }
3224
3421
  }
3225
3422
  };
@@ -3265,6 +3462,10 @@ ${output}` : output;
3265
3462
  settle(new Error(`the held test was terminated by signal ${signal}`));
3266
3463
  return;
3267
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
+ }
3268
3469
  settle(null, {
3269
3470
  exitCode: code,
3270
3471
  output: finalOutput(),
@@ -3275,7 +3476,7 @@ ${output}` : output;
3275
3476
  child.once("exit", (code, signal) => {
3276
3477
  if (killing)
3277
3478
  return;
3278
- captureReport();
3479
+ captureReport(code);
3279
3480
  if (timer)
3280
3481
  clearTimeout(timer);
3281
3482
  reapGroup(pgid);
@@ -3289,7 +3490,9 @@ ${output}` : output;
3289
3490
  terminateGroup(child, {
3290
3491
  sigintTimeoutMs: ORACLE_SIGINT_GRACE_MS,
3291
3492
  sigtermTimeoutMs: ORACLE_SIGTERM_GRACE_MS
3292
- }).catch(() => {}).then(() => {
3493
+ }).catch(() => {}).then(async () => {
3494
+ if (containerName)
3495
+ await removeSandboxContainer(containerName);
3293
3496
  settle(new Error(`the held test did not finish within ${timeoutMs}ms`));
3294
3497
  });
3295
3498
  }, timeoutMs);
@@ -3297,7 +3500,7 @@ ${output}` : output;
3297
3500
  }
3298
3501
 
3299
3502
  // src/oracle-collector.ts
3300
- var TAG6 = "oracle-collector";
3503
+ var TAG7 = "oracle-collector";
3301
3504
 
3302
3505
  class HeldOracleCollector {
3303
3506
  deps;
@@ -3308,7 +3511,7 @@ class HeldOracleCollector {
3308
3511
  const stageId = this.oracleStageId(context);
3309
3512
  if (!stageId) {
3310
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.";
3311
- log.warn(TAG6, `${reason} — blocked (config)`);
3514
+ log.warn(TAG7, `${reason} — blocked (config)`);
3312
3515
  return {
3313
3516
  result: "blocked",
3314
3517
  structured: { reason, ...GATE_CONFIG_ERROR_MARK }
@@ -3316,7 +3519,7 @@ class HeldOracleCollector {
3316
3519
  }
3317
3520
  const oracle = await this.deps.fetchOracle(context.cardId, stageId, this.deps.sessionId);
3318
3521
  if (!oracle) {
3319
- log.info(TAG6, `No oracle held for stage ${stageId} — blocked`);
3522
+ log.info(TAG7, `No oracle held for stage ${stageId} — blocked`);
3320
3523
  return {
3321
3524
  result: "blocked",
3322
3525
  structured: {
@@ -3333,13 +3536,13 @@ class HeldOracleCollector {
3333
3536
  };
3334
3537
  await this.deps.place(this.deps.repoPath, oracle);
3335
3538
  try {
3336
- 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 });
3337
3540
  const logLine = `Oracle run for ${oracle.path} exited ${exitCode}:
3338
3541
  ${output}`;
3339
3542
  if (exitCode === 0) {
3340
- log.info(TAG6, logLine);
3543
+ log.info(TAG7, logLine);
3341
3544
  } else {
3342
- log.warn(TAG6, logLine);
3545
+ log.warn(TAG7, logLine);
3343
3546
  }
3344
3547
  return this.verdict({
3345
3548
  oracle,
@@ -3350,12 +3553,14 @@ ${output}`;
3350
3553
  });
3351
3554
  } catch (err) {
3352
3555
  const message = errText(err);
3353
- 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)" : ""}`);
3354
3558
  return {
3355
3559
  result: "blocked",
3356
3560
  structured: {
3357
3561
  oracle: { path: oracle.path, ...identity },
3358
- error: message
3562
+ error: message,
3563
+ ...configError2 ? { reason: message, ...GATE_CONFIG_ERROR_MARK } : {}
3359
3564
  }
3360
3565
  };
3361
3566
  } finally {
@@ -3367,13 +3572,13 @@ ${output}`;
3367
3572
  await this.deps.remove(this.deps.repoPath, oracle);
3368
3573
  return;
3369
3574
  } catch (err) {
3370
- 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`);
3371
3576
  }
3372
3577
  try {
3373
3578
  await this.deps.remove(this.deps.repoPath, oracle);
3374
- 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`);
3375
3580
  } catch (err) {
3376
- 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.");
3377
3582
  }
3378
3583
  }
3379
3584
  }
@@ -3416,7 +3621,7 @@ class OracleRedCollector extends HeldOracleCollector {
3416
3621
  const graded = gradeOracleRed(report, exitCode);
3417
3622
  const base = { exitCode, path: oracle.path, ...identity };
3418
3623
  if (graded.outcome === "no_verdict") {
3419
- log.warn(TAG6, `Red gate for ${oracle.path}: ${graded.reason} — blocked`);
3624
+ log.warn(TAG7, `Red gate for ${oracle.path}: ${graded.reason} — blocked`);
3420
3625
  return {
3421
3626
  result: "blocked",
3422
3627
  structured: { oracle: base, reason: graded.reason }
@@ -3448,7 +3653,7 @@ init_log();
3448
3653
  init_run_containment();
3449
3654
  import { execFileSync as execFileSync2 } from "node:child_process";
3450
3655
  import { existsSync } from "node:fs";
3451
- var TAG7 = "pm";
3656
+ var TAG8 = "pm";
3452
3657
  var cached = null;
3453
3658
  function detectPackageManager() {
3454
3659
  if (cached)
@@ -3470,7 +3675,7 @@ function detectPackageManager() {
3470
3675
  } else {
3471
3676
  cached = "npm";
3472
3677
  }
3473
- log.info(TAG7, `Detected package manager: ${cached}`);
3678
+ log.info(TAG8, `Detected package manager: ${cached}`);
3474
3679
  return cached;
3475
3680
  }
3476
3681
  function installCommand(ignoreScripts = false) {
@@ -3499,7 +3704,7 @@ function spawnRunArgs(script, ...extra) {
3499
3704
  init_log();
3500
3705
  import { execFileSync as execFileSync3 } from "node:child_process";
3501
3706
  import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
3502
- var TAG8 = "project-type";
3707
+ var TAG9 = "project-type";
3503
3708
  var _cache = new Map;
3504
3709
  function _resetCache() {
3505
3710
  _cache.clear();
@@ -3510,7 +3715,7 @@ function detect(dir) {
3510
3715
  return cached2;
3511
3716
  const result = detectUncached(dir);
3512
3717
  _cache.set(dir, result);
3513
- log.info(TAG8, `Detected project type in ${dir}: ${result.kind}`);
3718
+ log.info(TAG9, `Detected project type in ${dir}: ${result.kind}`);
3514
3719
  return result;
3515
3720
  }
3516
3721
  function detectUncached(dir) {
@@ -3604,13 +3809,13 @@ function hasNodeTestScript(dir) {
3604
3809
  const pkg = JSON.parse(readFileSync3(`${dir}/package.json`, "utf-8"));
3605
3810
  script = pkg.scripts?.test;
3606
3811
  } catch (err) {
3607
- 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}`);
3608
3813
  return false;
3609
3814
  }
3610
3815
  if (typeof script !== "string" || script.trim().length === 0)
3611
3816
  return false;
3612
3817
  if (NPM_PLACEHOLDER_TEST.test(script)) {
3613
- 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`);
3614
3819
  return false;
3615
3820
  }
3616
3821
  return true;
@@ -3621,7 +3826,7 @@ function firstNodeScript(dir, candidates) {
3621
3826
  const pkg = JSON.parse(readFileSync3(`${dir}/package.json`, "utf-8"));
3622
3827
  scripts = pkg.scripts ?? {};
3623
3828
  } catch (err) {
3624
- 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}`);
3625
3830
  return null;
3626
3831
  }
3627
3832
  for (const name of candidates) {
@@ -3640,7 +3845,7 @@ function xcodeBuildCommand(pt) {
3640
3845
  return null;
3641
3846
  const scheme = resolveXcodeScheme(pt);
3642
3847
  if (!scheme) {
3643
- 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)");
3644
3849
  return null;
3645
3850
  }
3646
3851
  const containerFlag = pt.xcodeIsWorkspace ? "-workspace" : "-project";
@@ -3668,108 +3873,11 @@ function resolveXcodeScheme(pt) {
3668
3873
  const schemes = pt.xcodeIsWorkspace ? parsed.workspace?.schemes ?? [] : parsed.project?.schemes ?? [];
3669
3874
  return schemes[0] ?? null;
3670
3875
  } catch (err) {
3671
- 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}`);
3672
3877
  return null;
3673
3878
  }
3674
3879
  }
3675
3880
 
3676
- // src/repair-sandbox.ts
3677
- init_log();
3678
- import { randomUUID } from "node:crypto";
3679
- import { promisify as promisify2 } from "node:util";
3680
- var TAG9 = "repair-sandbox";
3681
- async function dockerExec(argv, opts) {
3682
- const { execFile: execFile2 } = await import("node:child_process");
3683
- return promisify2(execFile2)("docker", argv, {
3684
- encoding: "utf-8",
3685
- ...opts
3686
- });
3687
- }
3688
- var MAX_OUTPUT_BUFFER2 = 20971520;
3689
- var PROBE_TIMEOUT_MS = 1e4;
3690
- var SANDBOX_MOUNT = "/repo";
3691
- var SANDBOX_MEMORY = "4g";
3692
- var SANDBOX_PIDS = "512";
3693
- var dockerProbe = null;
3694
- async function sandboxAvailable() {
3695
- if (dockerProbe === true)
3696
- return true;
3697
- try {
3698
- await dockerExec(["version", "--format", "{{.Server.Version}}"], {
3699
- timeout: PROBE_TIMEOUT_MS
3700
- });
3701
- dockerProbe = true;
3702
- } catch {
3703
- dockerProbe = false;
3704
- }
3705
- return dockerProbe;
3706
- }
3707
- function __resetSandboxProbe() {
3708
- dockerProbe = null;
3709
- }
3710
- async function removeContainer(name) {
3711
- try {
3712
- await dockerExec(["rm", "--force", name], { timeout: PROBE_TIMEOUT_MS });
3713
- log.warn(TAG9, `removed the timed-out sandbox container ${name}`);
3714
- } catch {}
3715
- }
3716
- function sandboxRunArgs(image, worktree, command, name) {
3717
- return [
3718
- "run",
3719
- "--rm",
3720
- ...name ? ["--name", name] : [],
3721
- "--network=none",
3722
- "--cap-drop=ALL",
3723
- "--security-opt=no-new-privileges",
3724
- `--memory=${SANDBOX_MEMORY}`,
3725
- `--pids-limit=${SANDBOX_PIDS}`,
3726
- ...typeof process.getuid === "function" && typeof process.getgid === "function" ? ["--user", `${process.getuid()}:${process.getgid()}`] : [],
3727
- "--env",
3728
- "HOME=/tmp",
3729
- "--volume",
3730
- `${worktree}:${SANDBOX_MOUNT}`,
3731
- "--workdir",
3732
- SANDBOX_MOUNT,
3733
- "--entrypoint",
3734
- command.cmd,
3735
- image,
3736
- ...command.args
3737
- ];
3738
- }
3739
- async function runInSandbox(args) {
3740
- const name = `harmony-repair-${randomUUID()}`;
3741
- const argv = sandboxRunArgs(args.image, args.worktree, args.command, name);
3742
- log.info(TAG9, `sandbox: ${args.command.cmd} ${args.command.args.join(" ")} (image ${args.image})`);
3743
- try {
3744
- const { stdout } = await dockerExec(argv, {
3745
- timeout: args.timeoutMs,
3746
- maxBuffer: MAX_OUTPUT_BUFFER2
3747
- });
3748
- return { passed: true, output: stdout ?? "" };
3749
- } catch (err) {
3750
- const e = err;
3751
- const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
3752
- if (typeof e.code !== "number") {
3753
- const timedOut = e.killed === true || e.signal != null;
3754
- if (timedOut)
3755
- await removeContainer(name);
3756
- return {
3757
- passed: false,
3758
- output,
3759
- sandboxError: timedOut ? `the sandbox timed out after ${args.timeoutMs}ms` : `the sandbox did not run: ${e.message ?? "unknown error"}`
3760
- };
3761
- }
3762
- if (e.code === 125) {
3763
- return {
3764
- passed: false,
3765
- output,
3766
- sandboxError: `the sandbox could not start (image "${args.image}" missing or unusable)`
3767
- };
3768
- }
3769
- return { passed: false, output };
3770
- }
3771
- }
3772
-
3773
3881
  // src/revert-guard.ts
3774
3882
  init_log();
3775
3883
  init_run_containment();
@@ -3987,9 +4095,16 @@ async function runDeepReview(worktreePath, config, workerId) {
3987
4095
  }
3988
4096
  const port = config.verification.devServerBasePort + workerId;
3989
4097
  let devServer = null;
4098
+ const launch = devServerLaunch({
4099
+ worktreePath,
4100
+ port,
4101
+ sandbox: verificationSandbox(config)
4102
+ });
3990
4103
  try {
3991
- const [cmd, args] = spawnRunArgs("dev", "--port", String(port));
3992
- devServer = spawn2(cmd, args, {
4104
+ if (launch.containerName) {
4105
+ await removeSandboxContainer(launch.containerName);
4106
+ }
4107
+ devServer = spawn2(launch.cmd, launch.args, {
3993
4108
  cwd: worktreePath,
3994
4109
  stdio: ["ignore", "pipe", "pipe"],
3995
4110
  env: containedEnv()
@@ -4052,6 +4167,9 @@ async function runDeepReview(worktreePath, config, workerId) {
4052
4167
  if (devServer && !devServer.killed) {
4053
4168
  devServer.kill("SIGTERM");
4054
4169
  }
4170
+ if (launch.containerName) {
4171
+ await removeSandboxContainer(launch.containerName);
4172
+ }
4055
4173
  }
4056
4174
  }
4057
4175
  function attemptAutoFix(worktreePath, config, errors) {
@@ -4190,6 +4308,24 @@ class DevServerReadinessError extends Error {
4190
4308
  this.name = "DevServerReadinessError";
4191
4309
  }
4192
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;
4193
4329
  function waitForDevServer(proc, timeout) {
4194
4330
  return new Promise((resolve3, reject) => {
4195
4331
  let settled = false;
@@ -4219,7 +4355,7 @@ function waitForDevServer(proc, timeout) {
4219
4355
  }, timeout);
4220
4356
  const onData = (data) => {
4221
4357
  const text = data.toString();
4222
- if (text.includes("ready") || text.includes("localhost") || text.includes("Local:")) {
4358
+ if (DEV_SERVER_READY.test(text) || text.includes("localhost") || text.includes("Local:")) {
4223
4359
  settleResolve();
4224
4360
  }
4225
4361
  };
@@ -5181,7 +5317,7 @@ function branchAheadOfItsRemote(branchName, repoRoot = resolveRepoRoot()) {
5181
5317
  return false;
5182
5318
  }
5183
5319
  }
5184
- async function rescueUnpushedBranch(client, cardId, branchName, repoRoot = resolveRepoRoot()) {
5320
+ async function rescueUnpushedBranch(client, cardId, branchName, repoRoot = resolveRepoRoot(), agentSessionId) {
5185
5321
  const { getBranchWebUrl: getBranchWebUrl2, pushBranch: pushBranch2 } = await Promise.resolve().then(() => (init_git_pr(), exports_git_pr));
5186
5322
  try {
5187
5323
  pushBranch2(branchName, repoRoot);
@@ -5194,13 +5330,16 @@ async function rescueUnpushedBranch(client, cardId, branchName, repoRoot = resol
5194
5330
  const url = getBranchWebUrl2(branchName, repoRoot);
5195
5331
  const recover = url ? `View it at ${url} or recover locally: \`git fetch && git checkout ${branchName}\`` : `Recover it locally: \`git fetch && git checkout ${branchName}\``;
5196
5332
  const body = `⚠ Run ended before completion. Committed work was push-rescued to ` + `\`origin/${branchName}\` so it isn't lost. ${recover}`;
5197
- await client.addComment(cardId, body, { commentType: "message" });
5333
+ await client.addComment(cardId, body, {
5334
+ commentType: "message",
5335
+ ...agentSessionId ? { agentSessionId } : {}
5336
+ });
5198
5337
  } catch (err) {
5199
5338
  log.warn(TAG16, `push-rescue comment failed for ${branchName} (work is still safe on origin): ${err instanceof Error ? err.message : err}`);
5200
5339
  }
5201
5340
  return true;
5202
5341
  }
5203
- async function teardownWorktree(client, cardId, worktreePath, branchName) {
5342
+ async function teardownWorktree(client, cardId, worktreePath, branchName, agentSessionId) {
5204
5343
  let skipBranchDelete = false;
5205
5344
  if (branchName && cardId) {
5206
5345
  let repoRoot;
@@ -5211,7 +5350,7 @@ async function teardownWorktree(client, cardId, worktreePath, branchName) {
5211
5350
  return;
5212
5351
  }
5213
5352
  if (branchAheadOfItsRemote(branchName, repoRoot)) {
5214
- const ok = await rescueUnpushedBranch(client, cardId, branchName, repoRoot);
5353
+ const ok = await rescueUnpushedBranch(client, cardId, branchName, repoRoot, agentSessionId);
5215
5354
  if (!ok) {
5216
5355
  skipBranchDelete = true;
5217
5356
  log.error(TAG16, `Keeping local branch ${branchName} (push-rescue failed) to avoid orphaning its commit`);
@@ -5276,6 +5415,7 @@ export {
5276
5415
  reportFindings,
5277
5416
  renameRemoteBranch,
5278
5417
  removeWorktreeHoldingBranch,
5418
+ removeSandboxContainer,
5279
5419
  remove,
5280
5420
  remoteBranchExists,
5281
5421
  relayAgentEvent,
@@ -5307,6 +5447,8 @@ export {
5307
5447
  implementRunContainmentCliArgs,
5308
5448
  implementRunContainment,
5309
5449
  hostPersistencePaths,
5450
+ heldTestEnvKeysToStrip,
5451
+ heldOracleSandboxArgv,
5310
5452
  harmonyMcpServer,
5311
5453
  gradeOracleRed,
5312
5454
  gitMetadataDenyPaths,
@@ -5329,6 +5471,9 @@ export {
5329
5471
  extractCiReviewRequestedSha,
5330
5472
  extractAzurePrId,
5331
5473
  envKeysDroppedByLaunch,
5474
+ devServerSandboxArgs,
5475
+ devServerLaunch,
5476
+ devServerContainerName,
5332
5477
  detectPackageManager,
5333
5478
  detectGitProvider,
5334
5479
  detect,
@@ -5374,8 +5519,10 @@ export {
5374
5519
  SDK_ALLOWED_TOOLS,
5375
5520
  SANDBOX_STARTUP_GRACE_MS,
5376
5521
  SANDBOX_MOUNT,
5522
+ SANDBOX_DEV_SERVER_BIND,
5377
5523
  ReviewPassedCollector,
5378
5524
  ProjectSandboxOverrideError,
5525
+ OracleSandboxConfigError,
5379
5526
  OracleRedCollector,
5380
5527
  OracleCollector,
5381
5528
  ORACLE_RUNNER_HINTS,