@ai-development-environment/control-agent 0.0.28 → 0.0.30

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.
Files changed (2) hide show
  1. package/dist/control-agent.js +1525 -189
  2. package/package.json +1 -1
@@ -269,8 +269,8 @@ function createClient(options) {
269
269
  retryWait = async function randomisedExponentialBackoff(retries2) {
270
270
  const retryDelaySeconds = Math.pow(2, retries2);
271
271
  await new Promise(
272
- (resolve5) => setTimeout(
273
- resolve5,
272
+ (resolve6) => setTimeout(
273
+ resolve6,
274
274
  retryDelaySeconds * 1e3 + // add random timeout from 300ms to 3s
275
275
  Math.floor(Math.random() * (3e3 - 300) + 300)
276
276
  )
@@ -506,7 +506,7 @@ function createClient(options) {
506
506
  if (socket.readyState === WebSocketImpl.CLOSING) await throwOnClose;
507
507
  let release2 = () => {
508
508
  };
509
- const released = new Promise((resolve5) => release2 = resolve5);
509
+ const released = new Promise((resolve6) => release2 = resolve6);
510
510
  return [
511
511
  socket,
512
512
  release2,
@@ -669,7 +669,7 @@ function createClient(options) {
669
669
  const iterator = (async function* iterator2() {
670
670
  for (; ; ) {
671
671
  if (!pending.length) {
672
- await new Promise((resolve5) => deferred.resolve = resolve5);
672
+ await new Promise((resolve6) => deferred.resolve = resolve6);
673
673
  }
674
674
  while (pending.length) {
675
675
  yield pending.shift();
@@ -960,7 +960,7 @@ function createAgentSubscriptionClient(config) {
960
960
  shouldRetry: () => true,
961
961
  retryWait: async (retries) => {
962
962
  const delay = Math.min(3e4, 1e3 * 2 ** Math.min(retries, 5));
963
- await new Promise((resolve5) => setTimeout(resolve5, delay));
963
+ await new Promise((resolve6) => setTimeout(resolve6, delay));
964
964
  },
965
965
  on: {
966
966
  connected: () => console.log("Connected to control-plane WebSocket"),
@@ -1506,6 +1506,8 @@ var WORKTREE_BRANCH_JOB_KIND = "worktree.branch";
1506
1506
  var WORKTREE_MOVE_PUSH_JOB_KIND = "worktree.move.push";
1507
1507
  var WORKTREE_MOVE_CHECKOUT_JOB_KIND = "worktree.move.checkout";
1508
1508
  var WORKTREE_DELETE_JOB_KIND = "worktree.delete";
1509
+ var WORKTREE_DIFF_JOB_KIND = "worktree.diff.inspect";
1510
+ var WORKTREE_DIFF_ASSET_JOB_KIND = "worktree.diff.asset";
1509
1511
  var WORKTREE_JOB_KINDS = [
1510
1512
  WORKTREE_INSPECT_JOB_KIND,
1511
1513
  WORKTREE_OPERATION_JOB_KIND,
@@ -1513,7 +1515,9 @@ var WORKTREE_JOB_KINDS = [
1513
1515
  WORKTREE_BRANCH_JOB_KIND,
1514
1516
  WORKTREE_MOVE_PUSH_JOB_KIND,
1515
1517
  WORKTREE_MOVE_CHECKOUT_JOB_KIND,
1516
- WORKTREE_DELETE_JOB_KIND
1518
+ WORKTREE_DELETE_JOB_KIND,
1519
+ WORKTREE_DIFF_JOB_KIND,
1520
+ WORKTREE_DIFF_ASSET_JOB_KIND
1517
1521
  ];
1518
1522
  var DEFAULT_JIRA_BRANCH_REGEX = String.raw`\b([A-Z][A-Z0-9_]*-\d+)\b`;
1519
1523
  var WORKTREE_OPERATIONS = [
@@ -1534,6 +1538,13 @@ function validGitBranchName(value) {
1534
1538
  }
1535
1539
  return value.split("/").every((part) => part && !part.startsWith(".") && !part.endsWith(".lock"));
1536
1540
  }
1541
+ var WORKTREE_DIFF_SCOPES = [
1542
+ "STAGED",
1543
+ "UNSTAGED",
1544
+ "UNTRACKED",
1545
+ "COMMIT",
1546
+ "BRANCH"
1547
+ ];
1537
1548
  function objectValue4(value, name) {
1538
1549
  if (!value || typeof value !== "object" || Array.isArray(value)) {
1539
1550
  throw new Error(`${name} must be an object`);
@@ -1550,6 +1561,13 @@ function nullableString3(value, name) {
1550
1561
  if (value === null || value === void 0) return null;
1551
1562
  return stringValue4(value, name);
1552
1563
  }
1564
+ function safeRelativePath(value, name) {
1565
+ const path = stringValue4(value, name).replaceAll("\\", "/");
1566
+ if (path.startsWith("/") || /^[A-Za-z]:\//.test(path) || path.split("/").includes("..") || path.includes("\0")) {
1567
+ throw new Error(`${name} must stay within the worktree`);
1568
+ }
1569
+ return path;
1570
+ }
1553
1571
  function worktreeBranchJobPayload(value) {
1554
1572
  const payload = objectValue4(value, "worktree branch payload");
1555
1573
  const allowed = /* @__PURE__ */ new Set([
@@ -1858,6 +1876,73 @@ function worktreeJobPayload(value) {
1858
1876
  ...editorVariant === void 0 ? {} : { editorVariant }
1859
1877
  };
1860
1878
  }
1879
+ function worktreeDiffPayload(value) {
1880
+ const payload = objectValue4(value, "worktree diff payload");
1881
+ const allowed = /* @__PURE__ */ new Set([
1882
+ "codebaseId",
1883
+ "folder",
1884
+ "gitDirectory",
1885
+ "expectedOrigin",
1886
+ "baseBranch",
1887
+ "scope",
1888
+ "path",
1889
+ "previousPath",
1890
+ "commitSha",
1891
+ "uploadId",
1892
+ "side"
1893
+ ]);
1894
+ const unexpected = Object.keys(payload).find((key) => !allowed.has(key));
1895
+ if (unexpected) {
1896
+ throw new Error(`Unexpected worktree diff payload field: ${unexpected}`);
1897
+ }
1898
+ if (!WORKTREE_DIFF_SCOPES.includes(payload.scope)) {
1899
+ throw new Error("worktree diff payload.scope is invalid");
1900
+ }
1901
+ if (payload.side !== null && payload.side !== void 0 && payload.side !== "BEFORE" && payload.side !== "AFTER") {
1902
+ throw new Error("worktree diff payload.side is invalid");
1903
+ }
1904
+ const path = payload.path === null || payload.path === void 0 ? null : safeRelativePath(payload.path, "worktree diff payload.path");
1905
+ const previousPath = payload.previousPath === null || payload.previousPath === void 0 ? null : safeRelativePath(
1906
+ payload.previousPath,
1907
+ "worktree diff payload.previousPath"
1908
+ );
1909
+ const scope = payload.scope;
1910
+ const commitSha = nullableString3(
1911
+ payload.commitSha,
1912
+ "worktree diff payload.commitSha"
1913
+ );
1914
+ if (scope === "COMMIT" && !commitSha) {
1915
+ throw new Error("Commit diffs require a commit SHA");
1916
+ }
1917
+ return {
1918
+ codebaseId: stringValue4(
1919
+ payload.codebaseId,
1920
+ "worktree diff payload.codebaseId"
1921
+ ),
1922
+ folder: stringValue4(payload.folder, "worktree diff payload.folder"),
1923
+ gitDirectory: stringValue4(
1924
+ payload.gitDirectory,
1925
+ "worktree diff payload.gitDirectory"
1926
+ ),
1927
+ expectedOrigin: stringValue4(
1928
+ payload.expectedOrigin,
1929
+ "worktree diff payload.expectedOrigin"
1930
+ ),
1931
+ baseBranch: stringValue4(
1932
+ payload.baseBranch,
1933
+ "worktree diff payload.baseBranch"
1934
+ ),
1935
+ scope,
1936
+ path,
1937
+ previousPath,
1938
+ commitSha,
1939
+ uploadId: nullableString3(
1940
+ payload.uploadId,
1941
+ "worktree diff payload.uploadId"
1942
+ ),
1943
+ side: payload.side ?? null
1944
+ };
1945
+ }
1861
1946
  function worktreeWatchJobPayload(value) {
1862
1947
  const payload = objectValue4(value, "worktree watch payload");
1863
1948
  const allowed = /* @__PURE__ */ new Set([
@@ -2201,6 +2286,8 @@ var IOS_BUILD_DELETE_JOB_KIND = "ios.build.delete";
2201
2286
  var IOS_ARTIFACT_DOWNLOAD_JOB_KIND = "ios.artifact.download";
2202
2287
  var IOS_DEPLOY_JOB_KIND = "ios.build.deploy";
2203
2288
  var IOS_EXPORT_JOB_KIND = "ios.archive.export";
2289
+ var IOS_TEST_RESULTS_JOB_KIND = "ios.test-results.parse";
2290
+ var IOS_COVERAGE_REPORT_JOB_KIND = "ios.coverage.generate";
2204
2291
  var IOS_BUILD_JOB_KINDS = [
2205
2292
  IOS_SOURCE_DISCOVER_JOB_KIND,
2206
2293
  IOS_SOURCE_PARSE_JOB_KIND,
@@ -2210,7 +2297,9 @@ var IOS_BUILD_JOB_KINDS = [
2210
2297
  IOS_BUILD_DELETE_JOB_KIND,
2211
2298
  IOS_ARTIFACT_DOWNLOAD_JOB_KIND,
2212
2299
  IOS_DEPLOY_JOB_KIND,
2213
- IOS_EXPORT_JOB_KIND
2300
+ IOS_EXPORT_JOB_KIND,
2301
+ IOS_TEST_RESULTS_JOB_KIND,
2302
+ IOS_COVERAGE_REPORT_JOB_KIND
2214
2303
  ];
2215
2304
  var BUILD_ACTIONS = [
2216
2305
  "BUILD",
@@ -2259,6 +2348,7 @@ var DEFAULT_BUILD_ADVANCED_SETTINGS = {
2259
2348
  allowProvisioningDeviceRegistration: false,
2260
2349
  testPlan: null,
2261
2350
  codeCoverage: false,
2351
+ parseTestResults: true,
2262
2352
  parallelTesting: null,
2263
2353
  parallelTestingWorkers: null,
2264
2354
  onlyTesting: [],
@@ -2268,6 +2358,7 @@ var DEFAULT_BUILD_ADVANCED_SETTINGS = {
2268
2358
  priorTestProductsPath: null,
2269
2359
  priorXctestrunPath: null
2270
2360
  };
2361
+ var BUILD_REPORT_KINDS = ["TEST_RESULTS", "CODE_COVERAGE"];
2271
2362
  function objectValue6(value, name) {
2272
2363
  if (!value || typeof value !== "object" || Array.isArray(value)) {
2273
2364
  throw new Error(`${name} must be an object`);
@@ -2293,7 +2384,7 @@ function enumValue3(value, values, name) {
2293
2384
  }
2294
2385
  return value;
2295
2386
  }
2296
- function safeRelativePath(value, name) {
2387
+ function safeRelativePath2(value, name) {
2297
2388
  const path = stringValue6(value, name);
2298
2389
  if (path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path) || path.split(/[\\/]/).includes("..") || path.includes("\0")) {
2299
2390
  throw new Error(`${name} must stay within the worktree`);
@@ -2318,13 +2409,14 @@ function worktreeIdentity(value) {
2318
2409
  value.expectedOrigin,
2319
2410
  "build payload.expectedOrigin"
2320
2411
  ),
2321
- headSha: nullableString4(value.headSha, "build payload.headSha")
2412
+ headSha: nullableString4(value.headSha, "build payload.headSha"),
2413
+ baseBranch: nullableString4(value.baseBranch, "build payload.baseBranch")
2322
2414
  };
2323
2415
  }
2324
2416
  function parseBuildSource(value) {
2325
2417
  const source = objectValue6(value, "build source");
2326
2418
  const kind = enumValue3(source.kind, BUILD_SOURCE_KINDS, "build source.kind");
2327
- const relativePath = safeRelativePath(
2419
+ const relativePath = safeRelativePath2(
2328
2420
  source.relativePath,
2329
2421
  "build source.relativePath"
2330
2422
  );
@@ -2443,6 +2535,10 @@ function parseBuildAdvancedSettings(value) {
2443
2535
  input.codeCoverage,
2444
2536
  "advanced settings.codeCoverage"
2445
2537
  ),
2538
+ parseTestResults: booleanValue2(
2539
+ input.parseTestResults,
2540
+ "advanced settings.parseTestResults"
2541
+ ),
2446
2542
  parallelTesting: parallel,
2447
2543
  parallelTestingWorkers: workers ?? null,
2448
2544
  onlyTesting: stringArray2(
@@ -2580,7 +2676,27 @@ function parseBuildJobPayload(value) {
2580
2676
  action,
2581
2677
  destination,
2582
2678
  advancedSettings,
2583
- scripts
2679
+ scripts,
2680
+ worktreeCoverage: input.worktreeCoverage === void 0 ? false : booleanValue2(
2681
+ input.worktreeCoverage,
2682
+ "build job payload.worktreeCoverage"
2683
+ )
2684
+ };
2685
+ }
2686
+ function parseBuildReportPayload(value) {
2687
+ const input = objectValue6(value, "build report payload");
2688
+ return {
2689
+ ...parseBuildDeletePayload(input),
2690
+ reportKind: enumValue3(
2691
+ input.reportKind,
2692
+ BUILD_REPORT_KINDS,
2693
+ "build report payload.reportKind"
2694
+ ),
2695
+ source: enumValue3(
2696
+ input.source,
2697
+ ["MANUAL", "WORKTREE"],
2698
+ "build report payload.source"
2699
+ )
2584
2700
  };
2585
2701
  }
2586
2702
  function parseBuildDeletePayload(value) {
@@ -2607,7 +2723,7 @@ function parseBuildArtifactDownloadPayload(value) {
2607
2723
  const identity = parseBuildDeletePayload(input);
2608
2724
  return {
2609
2725
  ...identity,
2610
- artifactRelativePath: safeRelativePath(
2726
+ artifactRelativePath: safeRelativePath2(
2611
2727
  input.artifactRelativePath,
2612
2728
  "build artifact download payload.artifactRelativePath"
2613
2729
  ),
@@ -2649,7 +2765,7 @@ function parseBuildDeploymentPayload(value) {
2649
2765
  input.artifactDirectory,
2650
2766
  "build deployment payload.artifactDirectory"
2651
2767
  ),
2652
- artifactRelativePath: safeRelativePath(
2768
+ artifactRelativePath: safeRelativePath2(
2653
2769
  input.artifactRelativePath,
2654
2770
  "build deployment payload.artifactRelativePath"
2655
2771
  ),
@@ -2717,7 +2833,7 @@ function parseBuildExportPayload(value) {
2717
2833
  input.artifactDirectory,
2718
2834
  "build export payload.artifactDirectory"
2719
2835
  ),
2720
- archiveRelativePath: safeRelativePath(
2836
+ archiveRelativePath: safeRelativePath2(
2721
2837
  input.archiveRelativePath,
2722
2838
  "build export payload.archiveRelativePath"
2723
2839
  ),
@@ -2756,7 +2872,7 @@ function collectInventory() {
2756
2872
  // src/process-runner.ts
2757
2873
  import { spawn } from "node:child_process";
2758
2874
  async function runProcess(options) {
2759
- return new Promise((resolve5, reject) => {
2875
+ return new Promise((resolve6, reject) => {
2760
2876
  let sequence = 0;
2761
2877
  let timedOut = false;
2762
2878
  let cancelled = false;
@@ -2833,7 +2949,7 @@ async function runProcess(options) {
2833
2949
  clearTimeout(timeout);
2834
2950
  options.signal.removeEventListener("abort", abort);
2835
2951
  void logChain.then(
2836
- () => resolve5({ exitCode, signal, timedOut, cancelled })
2952
+ () => resolve6({ exitCode, signal, timedOut, cancelled })
2837
2953
  );
2838
2954
  });
2839
2955
  });
@@ -2925,9 +3041,9 @@ import { basename, dirname as dirname2, isAbsolute, join as join2, resolve } fro
2925
3041
 
2926
3042
  // src/capture-command.ts
2927
3043
  import { spawn as spawn2 } from "node:child_process";
2928
- var MAX_OUTPUT_BYTES = 1024 * 1024;
3044
+ var MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
2929
3045
  function captureCommand(options) {
2930
- return new Promise((resolve5, reject) => {
3046
+ return new Promise((resolve6, reject) => {
2931
3047
  let stdout = "";
2932
3048
  let stderr = "";
2933
3049
  let timedOut = false;
@@ -2935,15 +3051,19 @@ function captureCommand(options) {
2935
3051
  let settled = false;
2936
3052
  const child = spawn2(options.command, options.args, {
2937
3053
  shell: false,
2938
- stdio: ["ignore", "pipe", "pipe"],
3054
+ stdio: [
3055
+ "ignore",
3056
+ options.stdoutFileDescriptor === void 0 ? "pipe" : options.stdoutFileDescriptor,
3057
+ "pipe"
3058
+ ],
2939
3059
  env: options.env ?? process.env,
2940
3060
  cwd: options.cwd
2941
3061
  });
2942
3062
  const append = (current, chunk) => `${current}${String(chunk)}`.slice(0, MAX_OUTPUT_BYTES);
2943
- child.stdout.on("data", (chunk) => {
3063
+ child.stdout?.on("data", (chunk) => {
2944
3064
  stdout = append(stdout, chunk);
2945
3065
  });
2946
- child.stderr.on("data", (chunk) => {
3066
+ child.stderr?.on("data", (chunk) => {
2947
3067
  stderr = append(stderr, chunk);
2948
3068
  });
2949
3069
  const terminate = () => {
@@ -2975,7 +3095,7 @@ function captureCommand(options) {
2975
3095
  settled = true;
2976
3096
  clearTimeout(timeout);
2977
3097
  options.signal.removeEventListener("abort", abort);
2978
- resolve5({
3098
+ resolve6({
2979
3099
  exitCode,
2980
3100
  signal,
2981
3101
  timedOut,
@@ -2997,7 +3117,7 @@ var successfulProcess = {
2997
3117
  function errorMessage(error) {
2998
3118
  return error instanceof Error ? error.message : String(error);
2999
3119
  }
3000
- function safeRelativePath2(value) {
3120
+ function safeRelativePath3(value) {
3001
3121
  if (!value || value.includes("\0") || isAbsolute(value)) return false;
3002
3122
  const normalized = value.replaceAll("\\", "/");
3003
3123
  return normalized !== "." && normalized.split("/").every((part) => part && part !== ".." && part !== ".");
@@ -3012,7 +3132,7 @@ async function configuredRoots(payload) {
3012
3132
  }
3013
3133
  return [resolve(payload.path)];
3014
3134
  }
3015
- if (!payload.path || !safeRelativePath2(payload.path)) {
3135
+ if (!payload.path || !safeRelativePath3(payload.path)) {
3016
3136
  throw new Error("Relative Derived Data mode requires a safe relative path");
3017
3137
  }
3018
3138
  return payload.worktrees.map(
@@ -4430,9 +4550,204 @@ var applySkills = async (payload) => {
4430
4550
  };
4431
4551
 
4432
4552
  // src/handlers/worktrees.ts
4433
- import { watch } from "node:fs";
4434
- import { readFile as readFile3, realpath as realpath4, stat as stat4 } from "node:fs/promises";
4435
- import { basename as basename2, dirname as dirname5, join as join5, relative as relative2 } from "node:path";
4553
+ import { createWriteStream, watch } from "node:fs";
4554
+ import { mkdtemp, open, readFile as readFile3, realpath as realpath4, rm as rm3, stat as stat4 } from "node:fs/promises";
4555
+ import { tmpdir } from "node:os";
4556
+ import { basename as basename2, dirname as dirname5, join as join5, relative as relative3 } from "node:path";
4557
+ import { spawn as spawn4 } from "node:child_process";
4558
+
4559
+ // src/git-code-state.ts
4560
+ import { createHash as createHash2 } from "node:crypto";
4561
+ import { createReadStream as createReadStream2 } from "node:fs";
4562
+ import { lstat as lstat3, readlink } from "node:fs/promises";
4563
+ import { isAbsolute as isAbsolute3, relative as relative2, resolve as resolve4, sep as sep2 } from "node:path";
4564
+ import { spawn as spawn3 } from "node:child_process";
4565
+ function gitEnvironment() {
4566
+ return {
4567
+ ...process.env,
4568
+ GIT_TERMINAL_PROMPT: "0",
4569
+ GIT_OPTIONAL_LOCKS: "0"
4570
+ };
4571
+ }
4572
+ function remainingTimeoutMs(deadline) {
4573
+ return Math.max(1, deadline - Date.now());
4574
+ }
4575
+ function containedPath(folder, path) {
4576
+ const absolutePath = resolve4(folder, path);
4577
+ const difference = relative2(folder, absolutePath);
4578
+ if (!difference || difference === ".." || difference.startsWith(`..${sep2}`) || isAbsolute3(difference)) {
4579
+ return null;
4580
+ }
4581
+ return absolutePath;
4582
+ }
4583
+ function hashGitDiff(hash, folder, timeoutMs, signal) {
4584
+ return new Promise((resolveResult, reject) => {
4585
+ let settled = false;
4586
+ let timedOut = false;
4587
+ const child = spawn3(
4588
+ "git",
4589
+ [
4590
+ "-C",
4591
+ folder,
4592
+ "--no-optional-locks",
4593
+ "diff",
4594
+ "--binary",
4595
+ "--no-ext-diff",
4596
+ "--no-textconv",
4597
+ "HEAD",
4598
+ "--"
4599
+ ],
4600
+ {
4601
+ env: gitEnvironment(),
4602
+ shell: false,
4603
+ stdio: ["ignore", "pipe", "pipe"]
4604
+ }
4605
+ );
4606
+ child.stdout.on("data", (chunk) => hash.update(chunk));
4607
+ child.stderr.resume();
4608
+ const terminate = () => {
4609
+ if (child.exitCode === null && !child.killed) child.kill("SIGTERM");
4610
+ };
4611
+ const timeout = setTimeout(() => {
4612
+ timedOut = true;
4613
+ terminate();
4614
+ }, timeoutMs);
4615
+ timeout.unref();
4616
+ const abort = () => terminate();
4617
+ signal.addEventListener("abort", abort, { once: true });
4618
+ if (signal.aborted) abort();
4619
+ child.once("error", (error) => {
4620
+ if (settled) return;
4621
+ settled = true;
4622
+ clearTimeout(timeout);
4623
+ signal.removeEventListener("abort", abort);
4624
+ reject(error);
4625
+ });
4626
+ child.once("close", (exitCode) => {
4627
+ if (settled) return;
4628
+ settled = true;
4629
+ clearTimeout(timeout);
4630
+ signal.removeEventListener("abort", abort);
4631
+ resolveResult(!timedOut && !signal.aborted && exitCode === 0);
4632
+ });
4633
+ });
4634
+ }
4635
+ async function worktreeCodeStateHash(folder, timeoutMs, signal) {
4636
+ const deadline = Date.now() + timeoutMs;
4637
+ const operation = new AbortController();
4638
+ const abort = () => operation.abort(signal.reason);
4639
+ signal.addEventListener("abort", abort, { once: true });
4640
+ if (signal.aborted) abort();
4641
+ const timeout = setTimeout(() => operation.abort(), Math.max(0, timeoutMs));
4642
+ timeout.unref();
4643
+ try {
4644
+ operation.signal.throwIfAborted();
4645
+ const [head, untracked, submodules] = await Promise.all([
4646
+ captureCommand({
4647
+ command: "git",
4648
+ args: ["-C", folder, "rev-parse", "HEAD"],
4649
+ timeoutMs: remainingTimeoutMs(deadline),
4650
+ signal: operation.signal,
4651
+ env: gitEnvironment()
4652
+ }),
4653
+ captureCommand({
4654
+ command: "git",
4655
+ args: [
4656
+ "-C",
4657
+ folder,
4658
+ "--no-optional-locks",
4659
+ "ls-files",
4660
+ "--others",
4661
+ "--exclude-standard",
4662
+ "-z"
4663
+ ],
4664
+ timeoutMs: remainingTimeoutMs(deadline),
4665
+ signal: operation.signal,
4666
+ env: gitEnvironment()
4667
+ }),
4668
+ captureCommand({
4669
+ command: "git",
4670
+ args: [
4671
+ "-C",
4672
+ folder,
4673
+ "--no-optional-locks",
4674
+ "submodule",
4675
+ "foreach",
4676
+ "--quiet",
4677
+ 'printf "%s\\0" "$sm_path"'
4678
+ ],
4679
+ timeoutMs: remainingTimeoutMs(deadline),
4680
+ signal: operation.signal,
4681
+ env: gitEnvironment()
4682
+ })
4683
+ ]);
4684
+ if (head.exitCode !== 0 || untracked.exitCode !== 0 || submodules.exitCode !== 0 || operation.signal.aborted) {
4685
+ return null;
4686
+ }
4687
+ const hash = createHash2("sha256");
4688
+ hash.update("head\0");
4689
+ hash.update(head.stdout.trim());
4690
+ hash.update("\0diff\0");
4691
+ if (!await hashGitDiff(
4692
+ hash,
4693
+ folder,
4694
+ remainingTimeoutMs(deadline),
4695
+ operation.signal
4696
+ )) {
4697
+ return null;
4698
+ }
4699
+ hash.update("\0untracked\0");
4700
+ const paths = untracked.stdout.split("\0").filter(Boolean).sort();
4701
+ for (const path of paths) {
4702
+ operation.signal.throwIfAborted();
4703
+ const absolutePath = containedPath(folder, path);
4704
+ if (!absolutePath) return null;
4705
+ hash.update(path);
4706
+ hash.update("\0");
4707
+ const information = await lstat3(absolutePath);
4708
+ operation.signal.throwIfAborted();
4709
+ if (information.isSymbolicLink()) {
4710
+ hash.update("symlink\0");
4711
+ hash.update(await readlink(absolutePath));
4712
+ } else if (information.isFile()) {
4713
+ hash.update("file\0");
4714
+ hash.update((information.mode & 511).toString(8).padStart(3, "0"));
4715
+ hash.update("\0");
4716
+ for await (const chunk of createReadStream2(absolutePath, {
4717
+ signal: operation.signal
4718
+ })) {
4719
+ hash.update(chunk);
4720
+ }
4721
+ }
4722
+ hash.update("\0");
4723
+ }
4724
+ hash.update("submodules\0");
4725
+ const submodulePaths = submodules.stdout.split("\0").filter(Boolean).sort();
4726
+ for (const path of submodulePaths) {
4727
+ operation.signal.throwIfAborted();
4728
+ const absolutePath = containedPath(folder, path);
4729
+ if (!absolutePath) return null;
4730
+ const submoduleHash = await worktreeCodeStateHash(
4731
+ absolutePath,
4732
+ remainingTimeoutMs(deadline),
4733
+ operation.signal
4734
+ );
4735
+ if (!submoduleHash) return null;
4736
+ hash.update(path);
4737
+ hash.update("\0");
4738
+ hash.update(submoduleHash);
4739
+ hash.update("\0");
4740
+ }
4741
+ return hash.digest("hex");
4742
+ } catch {
4743
+ return null;
4744
+ } finally {
4745
+ clearTimeout(timeout);
4746
+ signal.removeEventListener("abort", abort);
4747
+ }
4748
+ }
4749
+
4750
+ // src/handlers/worktrees.ts
4436
4751
  var successfulProcess4 = {
4437
4752
  exitCode: 0,
4438
4753
  signal: null,
@@ -4440,6 +4755,16 @@ var successfulProcess4 = {
4440
4755
  cancelled: false
4441
4756
  };
4442
4757
  var WATCH_DEBOUNCE_MS = 500;
4758
+ var MAX_DIFF_BYTES = 2 * 1024 * 1024;
4759
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
4760
+ ".png",
4761
+ ".jpg",
4762
+ ".jpeg",
4763
+ ".gif",
4764
+ ".webp",
4765
+ ".avif",
4766
+ ".bmp"
4767
+ ]);
4443
4768
  var activeWorktreeWatches = /* @__PURE__ */ new Map();
4444
4769
  function statusChangeState(value) {
4445
4770
  const entries = value.split("\0").filter(Boolean);
@@ -4482,27 +4807,30 @@ async function flushWorktreeActivity(entry) {
4482
4807
  entry.pending = false;
4483
4808
  try {
4484
4809
  const signal = new AbortController().signal;
4485
- const [status2, branchResult, headResult] = await Promise.all([
4486
- git2(
4487
- entry.folder,
4488
- [
4489
- "--no-optional-locks",
4490
- "status",
4491
- "--porcelain=v1",
4492
- "-z",
4493
- "--untracked-files=all"
4494
- ],
4495
- entry.timeoutMs,
4496
- signal
4497
- ),
4498
- git2(
4499
- entry.folder,
4500
- ["symbolic-ref", "--short", "-q", "HEAD"],
4501
- entry.timeoutMs,
4502
- signal
4503
- ),
4504
- git2(entry.folder, ["rev-parse", "HEAD"], entry.timeoutMs, signal)
4505
- ]);
4810
+ const [status2, branchResult, headResult, codeStateHash] = await Promise.all(
4811
+ [
4812
+ git2(
4813
+ entry.folder,
4814
+ [
4815
+ "--no-optional-locks",
4816
+ "status",
4817
+ "--porcelain=v1",
4818
+ "-z",
4819
+ "--untracked-files=all"
4820
+ ],
4821
+ entry.timeoutMs,
4822
+ signal
4823
+ ),
4824
+ git2(
4825
+ entry.folder,
4826
+ ["symbolic-ref", "--short", "-q", "HEAD"],
4827
+ entry.timeoutMs,
4828
+ signal
4829
+ ),
4830
+ git2(entry.folder, ["rev-parse", "HEAD"], entry.timeoutMs, signal),
4831
+ worktreeCodeStateHash(entry.folder, entry.timeoutMs, signal)
4832
+ ]
4833
+ );
4506
4834
  const branch = branchResult.exitCode === 0 ? branchResult.stdout.trim() : null;
4507
4835
  const headSha = headResult.exitCode === 0 ? headResult.stdout.trim() : null;
4508
4836
  const headIdentity = headSha ? `${branch ?? ""}\0${headSha}` : null;
@@ -4510,6 +4838,7 @@ async function flushWorktreeActivity(entry) {
4510
4838
  const report = {
4511
4839
  codebaseId: entry.codebaseId,
4512
4840
  gitDirectory: entry.gitDirectory,
4841
+ codeStateHash,
4513
4842
  ...changes,
4514
4843
  pushStatus: await worktreePushStatus(
4515
4844
  entry.folder,
@@ -4718,10 +5047,11 @@ async function inspectWorktreeItem(folderValue2, rootFolder, baseBranch, primary
4718
5047
  return {
4719
5048
  gitDirectory: gitDir,
4720
5049
  folder,
4721
- relativePath: relative2(rootFolder, folder) || ".",
5050
+ relativePath: relative3(rootFolder, folder) || ".",
4722
5051
  primary,
4723
5052
  branch,
4724
5053
  headSha: headResult.exitCode === 0 ? headResult.stdout.trim() : null,
5054
+ codeStateHash: await worktreeCodeStateHash(folder, timeoutMs, signal),
4725
5055
  upstream,
4726
5056
  ahead: upstreamCounts.ahead,
4727
5057
  behind: upstreamCounts.behind,
@@ -4744,10 +5074,11 @@ async function inspectWorktreeItem(folderValue2, rootFolder, baseBranch, primary
4744
5074
  return {
4745
5075
  gitDirectory: folderValue2,
4746
5076
  folder: folderValue2,
4747
- relativePath: relative2(rootFolder, folderValue2) || ".",
5077
+ relativePath: relative3(rootFolder, folderValue2) || ".",
4748
5078
  primary,
4749
5079
  branch: null,
4750
5080
  headSha: null,
5081
+ codeStateHash: null,
4751
5082
  upstream: null,
4752
5083
  ahead: null,
4753
5084
  behind: null,
@@ -4889,7 +5220,8 @@ async function inspectChanges(folder, timeoutMs, signal) {
4889
5220
  const value = values[index];
4890
5221
  const code = value.slice(0, 2);
4891
5222
  const path = value.slice(3);
4892
- if ((code[0] === "R" || code[0] === "C") && values[index + 1]) index += 1;
5223
+ const previousPath = (code[0] === "R" || code[0] === "C") && values[index + 1] ? values[index + 1] : null;
5224
+ if (previousPath) index += 1;
4893
5225
  const untracked = code === "??";
4894
5226
  const conflicted = ["DD", "AU", "UD", "UA", "DU", "AA", "UU"].includes(
4895
5227
  code
@@ -4901,6 +5233,8 @@ async function inspectChanges(folder, timeoutMs, signal) {
4901
5233
  const untrackedCount = untracked ? await untrackedLines(folder, path) : null;
4902
5234
  changes.push({
4903
5235
  path,
5236
+ previousPath,
5237
+ changeType: untracked ? "ADDED" : conflicted ? "CONFLICTED" : code,
4904
5238
  staged,
4905
5239
  unstaged,
4906
5240
  untracked,
@@ -4913,6 +5247,65 @@ async function inspectChanges(folder, timeoutMs, signal) {
4913
5247
  }
4914
5248
  return { changes: changes.slice(0, 500), truncated: changes.length > 500 };
4915
5249
  }
5250
+ function imagePath(path) {
5251
+ const extension = path.slice(path.lastIndexOf(".")).toLocaleLowerCase();
5252
+ return IMAGE_EXTENSIONS.has(extension);
5253
+ }
5254
+ function parseNameStatus(value) {
5255
+ const entries = value.split("\0").filter(Boolean);
5256
+ const files = [];
5257
+ for (let index = 0; index < entries.length; ) {
5258
+ const status2 = entries[index++] ?? "M";
5259
+ const renamed = status2.startsWith("R") || status2.startsWith("C");
5260
+ const first = entries[index++] ?? "";
5261
+ const second = renamed ? entries[index++] ?? "" : null;
5262
+ const path = second ?? first;
5263
+ if (!path) continue;
5264
+ files.push({
5265
+ path,
5266
+ previousPath: renamed ? first : null,
5267
+ changeType: status2[0] ?? "M"
5268
+ });
5269
+ }
5270
+ return files;
5271
+ }
5272
+ async function diffFiles(folder, range, timeoutMs, signal) {
5273
+ const [status2, counts2] = await Promise.all([
5274
+ git2(folder, ["diff", "--name-status", "-z", range], timeoutMs, signal),
5275
+ git2(folder, ["diff", "--numstat", "-z", range], timeoutMs, signal)
5276
+ ]);
5277
+ requireSuccess2(status2, "Could not inspect changed files");
5278
+ requireSuccess2(counts2, "Could not inspect changed line counts");
5279
+ const numstat = parseNumstat(counts2.stdout);
5280
+ return parseNameStatus(status2.stdout).map((file) => {
5281
+ const [additions, deletions] = numstat.get(file.path) ?? [null, null];
5282
+ return {
5283
+ ...file,
5284
+ additions,
5285
+ deletions,
5286
+ binary: additions === null && deletions === null,
5287
+ image: imagePath(file.path)
5288
+ };
5289
+ });
5290
+ }
5291
+ async function inspectBranchChanges(folder, baseBranch, timeoutMs, signal) {
5292
+ const mergeBase = requireSuccess2(
5293
+ await git2(
5294
+ folder,
5295
+ ["merge-base", `refs/remotes/origin/${baseBranch}`, "HEAD"],
5296
+ timeoutMs,
5297
+ signal
5298
+ ),
5299
+ "Could not determine the base-branch merge base"
5300
+ ).stdout.trim();
5301
+ const files = await diffFiles(
5302
+ folder,
5303
+ `${mergeBase}..HEAD`,
5304
+ timeoutMs,
5305
+ signal
5306
+ );
5307
+ return { changes: files.slice(0, 500), truncated: files.length > 500 };
5308
+ }
4916
5309
  async function inspectCommits(folder, baseBranch, timeoutMs, signal) {
4917
5310
  const result = requireSuccess2(
4918
5311
  await git2(
@@ -4944,89 +5337,513 @@ async function inspectCommits(folder, baseBranch, timeoutMs, signal) {
4944
5337
  return { commits: commits.slice(0, 100), truncated: commits.length > 100 };
4945
5338
  }
4946
5339
  async function inspectWorktreeDetail(folder, baseBranch, timeoutMs, signal) {
4947
- const [commitResult, changeResult] = await Promise.all([
5340
+ const [commitResult, changeResult, branchResult] = await Promise.all([
4948
5341
  inspectCommits(folder, baseBranch, timeoutMs, signal),
4949
- inspectChanges(folder, timeoutMs, signal)
5342
+ inspectChanges(folder, timeoutMs, signal),
5343
+ inspectBranchChanges(folder, baseBranch, timeoutMs, signal)
4950
5344
  ]);
4951
5345
  return {
4952
5346
  commits: commitResult.commits,
4953
5347
  changes: changeResult.changes,
5348
+ branchChanges: branchResult.changes,
4954
5349
  commitsTruncated: commitResult.truncated,
4955
- changesTruncated: changeResult.truncated
5350
+ changesTruncated: changeResult.truncated,
5351
+ branchChangesTruncated: branchResult.truncated
4956
5352
  };
4957
5353
  }
4958
- var watchWorktree = async (payload, timeoutMs, signal, _onLog, context) => {
4959
- const input = worktreeWatchJobPayload(payload);
4960
- const current = activeWorktreeWatches.get(input.gitDirectory);
4961
- if (input.action === "STOP") {
4962
- if (current?.watchId === input.watchId) {
4963
- closeWorktreeWatch(current);
4964
- activeWorktreeWatches.delete(input.gitDirectory);
4965
- }
4966
- return successfulProcess4;
4967
- }
4968
- if (!context) throw new Error("Worktree activity reporting is unavailable");
4969
- const folder = await validateWorktree(input, timeoutMs, signal);
4970
- if (current?.watchId === input.watchId) return successfulProcess4;
4971
- if (current) {
4972
- closeWorktreeWatch(current);
4973
- activeWorktreeWatches.delete(input.gitDirectory);
4974
- }
4975
- const entry = {
4976
- watchId: input.watchId,
4977
- codebaseId: input.codebaseId,
4978
- gitDirectory: input.gitDirectory,
4979
- folder,
4980
- baseBranch: input.baseBranch,
4981
- timeoutMs: Math.min(timeoutMs, 3e4),
4982
- watchers: [],
4983
- reporter: context.reportWorktreeActivity,
4984
- timer: null,
4985
- reporting: false,
4986
- pending: false,
4987
- headIdentity: null
4988
- };
4989
- activeWorktreeWatches.set(input.gitDirectory, entry);
4990
- try {
4991
- for (const target2 of /* @__PURE__ */ new Set([folder, input.gitDirectory])) {
4992
- const watcher = watch(
4993
- target2,
4994
- { recursive: true },
4995
- () => scheduleWorktreeActivity(entry)
4996
- );
4997
- watcher.on("error", (error) => {
4998
- console.error(
4999
- `Worktree watcher failed for ${target2}:`,
5000
- error instanceof Error ? error.message : error
5001
- );
5002
- });
5003
- watcher.unref();
5004
- entry.watchers.push(watcher);
5005
- }
5006
- scheduleWorktreeActivity(entry);
5007
- } catch (error) {
5008
- closeWorktreeWatch(entry);
5009
- activeWorktreeWatches.delete(input.gitDirectory);
5010
- throw error;
5354
+ async function baseMergeCommit(folder, baseBranch, timeoutMs, signal) {
5355
+ return requireSuccess2(
5356
+ await git2(
5357
+ folder,
5358
+ ["merge-base", `refs/remotes/origin/${baseBranch}`, "HEAD"],
5359
+ timeoutMs,
5360
+ signal
5361
+ ),
5362
+ "Could not determine the base-branch merge base"
5363
+ ).stdout.trim();
5364
+ }
5365
+ async function validateDisplayedCommit(folder, baseBranch, commitSha, timeoutMs, signal) {
5366
+ if (!/^[0-9a-f]{7,64}$/i.test(commitSha)) {
5367
+ throw new Error("Commit SHA is invalid");
5011
5368
  }
5012
- return successfulProcess4;
5013
- };
5014
- var inspectWorktree = async (payload, timeoutMs, signal) => {
5015
- const input = worktreeJobPayload(payload);
5016
- const folder = await validateWorktree(input, timeoutMs, signal);
5017
- if (!input.baseBranch) throw new Error("A base branch is required");
5018
- return {
5019
- ...successfulProcess4,
5020
- detail: await inspectWorktreeDetail(
5369
+ const base = await baseMergeCommit(folder, baseBranch, timeoutMs, signal);
5370
+ const [afterBase, beforeHead] = await Promise.all([
5371
+ git2(
5021
5372
  folder,
5022
- input.baseBranch,
5373
+ ["merge-base", "--is-ancestor", base, commitSha],
5374
+ timeoutMs,
5375
+ signal
5376
+ ),
5377
+ git2(
5378
+ folder,
5379
+ ["merge-base", "--is-ancestor", commitSha, "HEAD"],
5023
5380
  timeoutMs,
5024
5381
  signal
5025
5382
  )
5026
- };
5027
- };
5028
- async function refExists(folder, ref, timeoutMs, signal) {
5029
- const result = await git2(
5383
+ ]);
5384
+ if (afterBase.exitCode !== 0 || beforeHead.exitCode !== 0) {
5385
+ throw new Error("Commit is outside the displayed branch history");
5386
+ }
5387
+ }
5388
+ async function parentCommit(folder, commitSha, timeoutMs, signal) {
5389
+ const result = await git2(
5390
+ folder,
5391
+ ["rev-parse", `${commitSha}^`],
5392
+ timeoutMs,
5393
+ signal
5394
+ );
5395
+ return result.exitCode === 0 ? result.stdout.trim() : null;
5396
+ }
5397
+ async function gitObjectExists(folder, specification, timeoutMs, signal) {
5398
+ return (await git2(folder, ["cat-file", "-e", specification], timeoutMs, signal)).exitCode === 0;
5399
+ }
5400
+ async function comparisonSides(input, folder, timeoutMs, signal) {
5401
+ if (!input.path) return { before: null, after: null };
5402
+ const path = input.path;
5403
+ const previousPath = input.previousPath ?? path;
5404
+ const currentPath = join5(folder, path);
5405
+ if (input.scope === "UNTRACKED") {
5406
+ return { before: null, after: { kind: "FILE", path: currentPath } };
5407
+ }
5408
+ if (input.scope === "STAGED") {
5409
+ return {
5410
+ before: { kind: "GIT", specification: `HEAD:${previousPath}` },
5411
+ after: { kind: "GIT", specification: `:${path}` }
5412
+ };
5413
+ }
5414
+ if (input.scope === "UNSTAGED") {
5415
+ return {
5416
+ before: { kind: "GIT", specification: `:${path}` },
5417
+ after: { kind: "FILE", path: currentPath }
5418
+ };
5419
+ }
5420
+ if (input.scope === "COMMIT") {
5421
+ const commitSha = input.commitSha;
5422
+ await validateDisplayedCommit(
5423
+ folder,
5424
+ input.baseBranch,
5425
+ commitSha,
5426
+ timeoutMs,
5427
+ signal
5428
+ );
5429
+ const parent = await parentCommit(folder, commitSha, timeoutMs, signal);
5430
+ return {
5431
+ before: parent ? { kind: "GIT", specification: `${parent}:${previousPath}` } : null,
5432
+ after: { kind: "GIT", specification: `${commitSha}:${path}` }
5433
+ };
5434
+ }
5435
+ const base = await baseMergeCommit(
5436
+ folder,
5437
+ input.baseBranch,
5438
+ timeoutMs,
5439
+ signal
5440
+ );
5441
+ return {
5442
+ before: { kind: "GIT", specification: `${base}:${previousPath}` },
5443
+ after: { kind: "GIT", specification: `HEAD:${path}` }
5444
+ };
5445
+ }
5446
+ function requestedDiffPaths(input) {
5447
+ if (!input.path) return [];
5448
+ if (input.previousPath && ["STAGED", "COMMIT", "BRANCH"].includes(input.scope)) {
5449
+ return [input.previousPath, input.path];
5450
+ }
5451
+ return [input.path];
5452
+ }
5453
+ async function readBoundedFile(path, limit) {
5454
+ const file = await open(path, "r");
5455
+ try {
5456
+ const metadata = await file.stat();
5457
+ if (!metadata.isFile()) throw new Error("Changed path is not a file");
5458
+ if (metadata.size > limit) {
5459
+ return { contents: Buffer.alloc(0), oversized: true };
5460
+ }
5461
+ const buffer = Buffer.allocUnsafe(limit + 1);
5462
+ let length = 0;
5463
+ while (length < buffer.length) {
5464
+ const { bytesRead } = await file.read(
5465
+ buffer,
5466
+ length,
5467
+ buffer.length - length,
5468
+ length
5469
+ );
5470
+ if (bytesRead === 0) break;
5471
+ length += bytesRead;
5472
+ }
5473
+ return {
5474
+ contents: buffer.subarray(0, Math.min(length, limit)),
5475
+ oversized: length > limit
5476
+ };
5477
+ } finally {
5478
+ await file.close();
5479
+ }
5480
+ }
5481
+ async function availableSide(side, folder, timeoutMs, signal) {
5482
+ if (!side) return false;
5483
+ if (side.kind === "GIT") {
5484
+ return gitObjectExists(folder, side.specification, timeoutMs, signal);
5485
+ }
5486
+ try {
5487
+ const resolved = await realpath4(side.path);
5488
+ const difference = relative3(folder, resolved);
5489
+ return difference !== ".." && !difference.startsWith("../") && (await stat4(resolved)).isFile();
5490
+ } catch {
5491
+ return false;
5492
+ }
5493
+ }
5494
+ async function inspectRequestedDiff(input, folder, timeoutMs, signal) {
5495
+ if (!input.path) {
5496
+ if (input.scope === "COMMIT") {
5497
+ await validateDisplayedCommit(
5498
+ folder,
5499
+ input.baseBranch,
5500
+ input.commitSha,
5501
+ timeoutMs,
5502
+ signal
5503
+ );
5504
+ const parent = await parentCommit(
5505
+ folder,
5506
+ input.commitSha,
5507
+ timeoutMs,
5508
+ signal
5509
+ );
5510
+ const files = await diffFiles(
5511
+ folder,
5512
+ parent ? `${parent}..${input.commitSha}` : input.commitSha,
5513
+ timeoutMs,
5514
+ signal
5515
+ );
5516
+ return { files: files.slice(0, 500), truncated: files.length > 500 };
5517
+ }
5518
+ if (input.scope === "BRANCH") {
5519
+ const result = await inspectBranchChanges(
5520
+ folder,
5521
+ input.baseBranch,
5522
+ timeoutMs,
5523
+ signal
5524
+ );
5525
+ return { files: result.changes, truncated: result.truncated };
5526
+ }
5527
+ throw new Error("A file path is required for this diff scope");
5528
+ }
5529
+ if (input.scope === "COMMIT") {
5530
+ await validateDisplayedCommit(
5531
+ folder,
5532
+ input.baseBranch,
5533
+ input.commitSha,
5534
+ timeoutMs,
5535
+ signal
5536
+ );
5537
+ }
5538
+ await validateChangedPath(input, folder, timeoutMs, signal);
5539
+ const args = ["diff", "--no-color", "--no-ext-diff", "--unified=3"];
5540
+ let oversized = false;
5541
+ if (input.scope === "STAGED") args.push("--cached");
5542
+ if (input.scope === "COMMIT") {
5543
+ const parent = await parentCommit(
5544
+ folder,
5545
+ input.commitSha,
5546
+ timeoutMs,
5547
+ signal
5548
+ );
5549
+ args.push(parent ?? `${input.commitSha}^`, input.commitSha);
5550
+ } else if (input.scope === "BRANCH") {
5551
+ args.push(
5552
+ await baseMergeCommit(folder, input.baseBranch, timeoutMs, signal),
5553
+ "HEAD"
5554
+ );
5555
+ }
5556
+ args.push("--", ...requestedDiffPaths(input));
5557
+ let patch;
5558
+ if (input.scope === "UNTRACKED") {
5559
+ const bounded = await readBoundedFile(
5560
+ join5(folder, input.path),
5561
+ MAX_DIFF_BYTES
5562
+ );
5563
+ const contents = bounded.contents;
5564
+ if (bounded.oversized || contents.includes(0)) {
5565
+ oversized = bounded.oversized;
5566
+ patch = "";
5567
+ } else {
5568
+ patch = [
5569
+ `diff --git a/${input.path} b/${input.path}`,
5570
+ "new file mode 100644",
5571
+ "--- /dev/null",
5572
+ `+++ b/${input.path}`,
5573
+ ...contents.toString("utf8").split("\n").map((line) => `+${line}`)
5574
+ ].join("\n");
5575
+ }
5576
+ } else {
5577
+ patch = requireSuccess2(
5578
+ await git2(folder, args, timeoutMs, signal),
5579
+ "Could not load the file diff"
5580
+ ).stdout;
5581
+ }
5582
+ const sides = await comparisonSides(input, folder, timeoutMs, signal);
5583
+ const [beforeAvailable, afterAvailable] = await Promise.all([
5584
+ availableSide(sides.before, folder, timeoutMs, signal),
5585
+ availableSide(sides.after, folder, timeoutMs, signal)
5586
+ ]);
5587
+ return {
5588
+ files: [],
5589
+ patch,
5590
+ image: imagePath(input.path),
5591
+ binary: !patch && !imagePath(input.path) && !oversized,
5592
+ truncated: oversized || Buffer.byteLength(patch) >= MAX_DIFF_BYTES,
5593
+ beforeAvailable,
5594
+ afterAvailable
5595
+ };
5596
+ }
5597
+ async function validateChangedPath(input, folder, timeoutMs, signal) {
5598
+ if (!input.path) throw new Error("A changed file path is required");
5599
+ let result;
5600
+ if (input.scope === "UNTRACKED") {
5601
+ result = await git2(
5602
+ folder,
5603
+ ["ls-files", "--others", "--exclude-standard", "-z", "--", input.path],
5604
+ timeoutMs,
5605
+ signal
5606
+ );
5607
+ } else {
5608
+ const args = ["diff", "--name-status", "-z"];
5609
+ if (input.scope === "STAGED") args.push("--cached");
5610
+ if (input.scope === "COMMIT") {
5611
+ await validateDisplayedCommit(
5612
+ folder,
5613
+ input.baseBranch,
5614
+ input.commitSha,
5615
+ timeoutMs,
5616
+ signal
5617
+ );
5618
+ const parent = await parentCommit(
5619
+ folder,
5620
+ input.commitSha,
5621
+ timeoutMs,
5622
+ signal
5623
+ );
5624
+ args.push(parent ?? `${input.commitSha}^`, input.commitSha);
5625
+ } else if (input.scope === "BRANCH") {
5626
+ args.push(
5627
+ await baseMergeCommit(folder, input.baseBranch, timeoutMs, signal),
5628
+ "HEAD"
5629
+ );
5630
+ }
5631
+ args.push("--", ...requestedDiffPaths(input));
5632
+ result = await git2(folder, args, timeoutMs, signal);
5633
+ }
5634
+ requireSuccess2(result, "Could not validate the changed file");
5635
+ const requested = input.scope === "UNTRACKED" ? result.stdout.split("\0").filter(Boolean).includes(input.path) : parseNameStatus(result.stdout).some(
5636
+ (file) => file.path === input.path && (!input.previousPath || file.previousPath === input.previousPath)
5637
+ );
5638
+ if (!requested) {
5639
+ throw new Error("The requested file is outside the displayed diff");
5640
+ }
5641
+ }
5642
+ var inspectWorktreeDiff = async (payload, timeoutMs, signal) => {
5643
+ const input = worktreeDiffPayload(payload);
5644
+ const folder = await validateWorktree(input, timeoutMs, signal);
5645
+ return {
5646
+ ...successfulProcess4,
5647
+ diff: await inspectRequestedDiff(input, folder, timeoutMs, signal)
5648
+ };
5649
+ };
5650
+ function imageContentType(path) {
5651
+ const extension = path.slice(path.lastIndexOf(".")).toLocaleLowerCase();
5652
+ return {
5653
+ ".png": "image/png",
5654
+ ".jpg": "image/jpeg",
5655
+ ".jpeg": "image/jpeg",
5656
+ ".gif": "image/gif",
5657
+ ".webp": "image/webp",
5658
+ ".avif": "image/avif",
5659
+ ".bmp": "image/bmp"
5660
+ }[extension] ?? "application/octet-stream";
5661
+ }
5662
+ async function writeGitObject(folder, specification, destination, timeoutMs, signal) {
5663
+ await new Promise((resolvePromise, rejectPromise) => {
5664
+ const child = spawn4(
5665
+ "git",
5666
+ ["-C", folder, "cat-file", "blob", specification],
5667
+ { stdio: ["ignore", "pipe", "pipe"] }
5668
+ );
5669
+ const output = createWriteStream(destination, { mode: 384 });
5670
+ let stderr = "";
5671
+ let settled = false;
5672
+ const fail = (error) => {
5673
+ if (settled) return;
5674
+ settled = true;
5675
+ rejectPromise(error instanceof Error ? error : new Error(String(error)));
5676
+ };
5677
+ child.stderr.on("data", (chunk) => {
5678
+ stderr = `${stderr}${String(chunk)}`.slice(0, 4e3);
5679
+ });
5680
+ child.stdout.pipe(output);
5681
+ child.once("error", fail);
5682
+ output.once("error", fail);
5683
+ const terminate = () => child.kill("SIGTERM");
5684
+ const timer = setTimeout(() => {
5685
+ terminate();
5686
+ fail(new Error("Image extraction timed out"));
5687
+ }, timeoutMs);
5688
+ timer.unref();
5689
+ const abort = () => {
5690
+ terminate();
5691
+ fail(new Error("Image extraction was cancelled"));
5692
+ };
5693
+ signal.addEventListener("abort", abort, { once: true });
5694
+ child.once("close", (code) => {
5695
+ clearTimeout(timer);
5696
+ signal.removeEventListener("abort", abort);
5697
+ if (settled) return;
5698
+ if (code !== 0) {
5699
+ fail(new Error(cleanError2(stderr || "Could not extract image")));
5700
+ return;
5701
+ }
5702
+ output.end(() => {
5703
+ if (settled) return;
5704
+ settled = true;
5705
+ resolvePromise();
5706
+ });
5707
+ });
5708
+ });
5709
+ }
5710
+ var downloadWorktreeDiffAsset = async (payload, timeoutMs, signal, _onLog, context) => {
5711
+ const input = worktreeDiffPayload(payload);
5712
+ if (!input.path || !input.uploadId || !input.side) {
5713
+ throw new Error("Diff image path, side, and upload ID are required");
5714
+ }
5715
+ if (!imagePath(input.path)) throw new Error("Diff asset is not an image");
5716
+ if (!context?.uploadBuildArtifact) {
5717
+ throw new Error("This agent cannot upload diff images");
5718
+ }
5719
+ const folder = await validateWorktree(input, timeoutMs, signal);
5720
+ await validateChangedPath(input, folder, timeoutMs, signal);
5721
+ const sides = await comparisonSides(input, folder, timeoutMs, signal);
5722
+ const selected = input.side === "BEFORE" ? sides.before : sides.after;
5723
+ if (!selected || !await availableSide(selected, folder, timeoutMs, signal)) {
5724
+ throw new Error("The requested image side is unavailable");
5725
+ }
5726
+ let uploadPath;
5727
+ let temporaryDirectory = null;
5728
+ try {
5729
+ if (selected.kind === "FILE") {
5730
+ uploadPath = await realpath4(selected.path);
5731
+ const difference = relative3(folder, uploadPath);
5732
+ if (difference === ".." || difference.startsWith("../")) {
5733
+ throw new Error("Diff image resolves outside the worktree");
5734
+ }
5735
+ } else {
5736
+ const size = requireSuccess2(
5737
+ await git2(
5738
+ folder,
5739
+ ["cat-file", "-s", selected.specification],
5740
+ timeoutMs,
5741
+ signal
5742
+ ),
5743
+ "Could not inspect diff image"
5744
+ ).stdout.trim();
5745
+ if (!/^\d+$/.test(size) || Number(size) > 20 * 1024 * 1024) {
5746
+ throw new Error("Diff image exceeds the 20 MiB limit");
5747
+ }
5748
+ temporaryDirectory = await mkdtemp(join5(tmpdir(), "ade-diff-image-"));
5749
+ uploadPath = join5(temporaryDirectory, basename2(input.path));
5750
+ await writeGitObject(
5751
+ folder,
5752
+ selected.specification,
5753
+ uploadPath,
5754
+ timeoutMs,
5755
+ signal
5756
+ );
5757
+ }
5758
+ const information = await stat4(uploadPath);
5759
+ if (!information.isFile() || information.size > 20 * 1024 * 1024) {
5760
+ throw new Error("Diff image exceeds the 20 MiB limit");
5761
+ }
5762
+ await context.uploadBuildArtifact({
5763
+ uploadId: input.uploadId,
5764
+ path: uploadPath,
5765
+ filename: basename2(input.path),
5766
+ contentType: imageContentType(input.path)
5767
+ });
5768
+ return successfulProcess4;
5769
+ } finally {
5770
+ if (temporaryDirectory) {
5771
+ await rm3(temporaryDirectory, { recursive: true, force: true });
5772
+ }
5773
+ }
5774
+ };
5775
+ var watchWorktree = async (payload, timeoutMs, signal, _onLog, context) => {
5776
+ const input = worktreeWatchJobPayload(payload);
5777
+ const current = activeWorktreeWatches.get(input.gitDirectory);
5778
+ if (input.action === "STOP") {
5779
+ if (current?.watchId === input.watchId) {
5780
+ closeWorktreeWatch(current);
5781
+ activeWorktreeWatches.delete(input.gitDirectory);
5782
+ }
5783
+ return successfulProcess4;
5784
+ }
5785
+ if (!context) throw new Error("Worktree activity reporting is unavailable");
5786
+ const folder = await validateWorktree(input, timeoutMs, signal);
5787
+ if (current?.watchId === input.watchId) return successfulProcess4;
5788
+ if (current) {
5789
+ closeWorktreeWatch(current);
5790
+ activeWorktreeWatches.delete(input.gitDirectory);
5791
+ }
5792
+ const entry = {
5793
+ watchId: input.watchId,
5794
+ codebaseId: input.codebaseId,
5795
+ gitDirectory: input.gitDirectory,
5796
+ folder,
5797
+ baseBranch: input.baseBranch,
5798
+ timeoutMs: Math.min(timeoutMs, 3e4),
5799
+ watchers: [],
5800
+ reporter: context.reportWorktreeActivity,
5801
+ timer: null,
5802
+ reporting: false,
5803
+ pending: false,
5804
+ headIdentity: null
5805
+ };
5806
+ activeWorktreeWatches.set(input.gitDirectory, entry);
5807
+ try {
5808
+ for (const target2 of /* @__PURE__ */ new Set([folder, input.gitDirectory])) {
5809
+ const watcher = watch(
5810
+ target2,
5811
+ { recursive: true },
5812
+ () => scheduleWorktreeActivity(entry)
5813
+ );
5814
+ watcher.on("error", (error) => {
5815
+ console.error(
5816
+ `Worktree watcher failed for ${target2}:`,
5817
+ error instanceof Error ? error.message : error
5818
+ );
5819
+ });
5820
+ watcher.unref();
5821
+ entry.watchers.push(watcher);
5822
+ }
5823
+ scheduleWorktreeActivity(entry);
5824
+ } catch (error) {
5825
+ closeWorktreeWatch(entry);
5826
+ activeWorktreeWatches.delete(input.gitDirectory);
5827
+ throw error;
5828
+ }
5829
+ return successfulProcess4;
5830
+ };
5831
+ var inspectWorktree = async (payload, timeoutMs, signal) => {
5832
+ const input = worktreeJobPayload(payload);
5833
+ const folder = await validateWorktree(input, timeoutMs, signal);
5834
+ if (!input.baseBranch) throw new Error("A base branch is required");
5835
+ return {
5836
+ ...successfulProcess4,
5837
+ detail: await inspectWorktreeDetail(
5838
+ folder,
5839
+ input.baseBranch,
5840
+ timeoutMs,
5841
+ signal
5842
+ )
5843
+ };
5844
+ };
5845
+ async function refExists(folder, ref, timeoutMs, signal) {
5846
+ const result = await git2(
5030
5847
  folder,
5031
5848
  ["show-ref", "--verify", "--quiet", ref],
5032
5849
  timeoutMs,
@@ -5752,30 +6569,32 @@ var operateWorktree = async (payload, timeoutMs, signal) => {
5752
6569
  };
5753
6570
 
5754
6571
  // src/handlers/builds.ts
5755
- import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
6572
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
5756
6573
  import {
5757
6574
  cp,
5758
6575
  mkdir as mkdir3,
5759
- mkdtemp,
6576
+ mkdtemp as mkdtemp2,
6577
+ open as open2,
5760
6578
  readdir as readdir4,
5761
6579
  readFile as readFile4,
5762
6580
  realpath as realpath5,
5763
- rm as rm3,
6581
+ rename as rename3,
6582
+ rm as rm4,
5764
6583
  stat as stat5,
5765
6584
  writeFile as writeFile3
5766
6585
  } from "node:fs/promises";
5767
- import { createWriteStream } from "node:fs";
5768
- import { tmpdir } from "node:os";
6586
+ import { createWriteStream as createWriteStream2 } from "node:fs";
6587
+ import { tmpdir as tmpdir2 } from "node:os";
5769
6588
  import {
5770
6589
  basename as basename3,
5771
6590
  dirname as dirname6,
5772
- isAbsolute as isAbsolute3,
6591
+ isAbsolute as isAbsolute4,
5773
6592
  join as join6,
5774
- relative as relative3,
5775
- resolve as resolve4,
5776
- sep as sep2
6593
+ relative as relative4,
6594
+ resolve as resolve5,
6595
+ sep as sep3
5777
6596
  } from "node:path";
5778
- import { spawn as spawn3 } from "node:child_process";
6597
+ import { spawn as spawn5 } from "node:child_process";
5779
6598
  var successfulProcess5 = {
5780
6599
  exitCode: 0,
5781
6600
  signal: null,
@@ -5806,6 +6625,22 @@ function command2(executable, args, timeoutMs, signal, cwd, env = process.env) {
5806
6625
  env
5807
6626
  });
5808
6627
  }
6628
+ async function commandToFile(executable, args, timeoutMs, signal, cwd, env, destination) {
6629
+ const output = await open2(destination, "wx", 384);
6630
+ try {
6631
+ return await captureCommand({
6632
+ command: executable,
6633
+ args,
6634
+ timeoutMs,
6635
+ signal,
6636
+ cwd,
6637
+ env,
6638
+ stdoutFileDescriptor: output.fd
6639
+ });
6640
+ } finally {
6641
+ await output.close();
6642
+ }
6643
+ }
5809
6644
  function requireSuccess3(result, fallback) {
5810
6645
  if (result.cancelled) throw new Error("Operation was cancelled");
5811
6646
  if (result.timedOut) throw new Error("Operation timed out");
@@ -5848,19 +6683,19 @@ async function validateWorktree2(input, timeoutMs, signal) {
5848
6683
  }
5849
6684
  return folder;
5850
6685
  }
5851
- function containedPath(root, relativePath) {
5852
- const target2 = resolve4(root, relativePath);
5853
- const difference = relative3(root, target2);
5854
- if (difference === ".." || difference.startsWith(`..${sep2}`) || isAbsolute3(difference)) {
6686
+ function containedPath2(root, relativePath) {
6687
+ const target2 = resolve5(root, relativePath);
6688
+ const difference = relative4(root, target2);
6689
+ if (difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute4(difference)) {
5855
6690
  throw new Error("Path must stay within the worktree");
5856
6691
  }
5857
6692
  return target2;
5858
6693
  }
5859
6694
  async function validateSource(root, source) {
5860
- const target2 = containedPath(root, source.relativePath);
6695
+ const target2 = containedPath2(root, source.relativePath);
5861
6696
  const resolved = await realpath5(target2);
5862
- const difference = relative3(root, resolved);
5863
- if (difference === ".." || difference.startsWith(`..${sep2}`) || isAbsolute3(difference)) {
6697
+ const difference = relative4(root, resolved);
6698
+ if (difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute4(difference)) {
5864
6699
  throw new Error("Build source resolves outside the worktree");
5865
6700
  }
5866
6701
  const information = await stat5(resolved);
@@ -5880,7 +6715,7 @@ async function discoverSourcesInFolder(folder) {
5880
6715
  for (const entry of await readdir4(current, { withFileTypes: true })) {
5881
6716
  if (entry.isSymbolicLink()) continue;
5882
6717
  const absolute = join6(current, entry.name);
5883
- const path = relative3(folder, absolute).split(sep2).join("/");
6718
+ const path = relative4(folder, absolute).split(sep3).join("/");
5884
6719
  if (entry.isDirectory()) {
5885
6720
  if (entry.name.endsWith(".xcodeproj")) {
5886
6721
  sources.push({ kind: "PROJECT", relativePath: path });
@@ -5971,15 +6806,15 @@ async function workspaceProjectPaths(workspacePath2, folder) {
5971
6806
  }).filter((path) => path.endsWith(".xcodeproj"));
5972
6807
  const projects = [];
5973
6808
  for (const location of locations) {
5974
- const candidate = resolve4(dirname6(workspacePath2), location);
5975
- const difference = relative3(folder, candidate);
5976
- if (difference === ".." || difference.startsWith(`..${sep2}`) || isAbsolute3(difference)) {
6809
+ const candidate = resolve5(dirname6(workspacePath2), location);
6810
+ const difference = relative4(folder, candidate);
6811
+ if (difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute4(difference)) {
5977
6812
  continue;
5978
6813
  }
5979
6814
  try {
5980
6815
  const resolved = await realpath5(candidate);
5981
- const resolvedDifference = relative3(folder, resolved);
5982
- if (resolvedDifference === ".." || resolvedDifference.startsWith(`..${sep2}`) || isAbsolute3(resolvedDifference) || !(await stat5(resolved)).isDirectory()) {
6816
+ const resolvedDifference = relative4(folder, resolved);
6817
+ if (resolvedDifference === ".." || resolvedDifference.startsWith(`..${sep3}`) || isAbsolute4(resolvedDifference) || !(await stat5(resolved)).isDirectory()) {
5983
6818
  continue;
5984
6819
  }
5985
6820
  projects.push(resolved);
@@ -6194,7 +7029,7 @@ function genericBuildDestinations(action) {
6194
7029
  ];
6195
7030
  }
6196
7031
  async function listPhysicalDevices(timeoutMs, signal) {
6197
- const directory = await mkdtemp(join6(tmpdir(), "ade-devices-"));
7032
+ const directory = await mkdtemp2(join6(tmpdir2(), "ade-devices-"));
6198
7033
  const output = join6(directory, "devices.json");
6199
7034
  try {
6200
7035
  const result = await command2(
@@ -6215,7 +7050,7 @@ async function listPhysicalDevices(timeoutMs, signal) {
6215
7050
  if (result.exitCode !== 0) return [];
6216
7051
  return physicalDestinations(JSON.parse(await readFile4(output, "utf8")));
6217
7052
  } finally {
6218
- await rm3(directory, { recursive: true, force: true });
7053
+ await rm4(directory, { recursive: true, force: true });
6219
7054
  }
6220
7055
  }
6221
7056
  var inspectBuildDestinations = async (payload, timeoutMs, signal) => {
@@ -6239,12 +7074,6 @@ var inspectBuildDestinations = async (payload, timeoutMs, signal) => {
6239
7074
  folder
6240
7075
  );
6241
7076
  requireSuccess3(preflight, "The saved scheme or configuration is unavailable");
6242
- if (GENERIC_BUILD_DESTINATION_ACTIONS.includes(input.action)) {
6243
- return {
6244
- ...successfulProcess5,
6245
- destinations: genericBuildDestinations(input.action)
6246
- };
6247
- }
6248
7077
  const [simulators, physical] = await Promise.all([
6249
7078
  command2(
6250
7079
  "xcrun",
@@ -6258,6 +7087,7 @@ var inspectBuildDestinations = async (payload, timeoutMs, signal) => {
6258
7087
  return {
6259
7088
  ...successfulProcess5,
6260
7089
  destinations: [
7090
+ ...genericBuildDestinations(input.action),
6261
7091
  ...simulators.exitCode === 0 ? simulatorDestinations(JSON.parse(simulators.stdout)) : [],
6262
7092
  ...physical
6263
7093
  ]
@@ -6311,7 +7141,7 @@ var BuildLogger = class {
6311
7141
  constructor(buildId, context, rawLogPath, env) {
6312
7142
  this.buildId = buildId;
6313
7143
  this.context = context;
6314
- this.stream = createWriteStream(rawLogPath, { flags: "a", mode: 384 });
7144
+ this.stream = createWriteStream2(rawLogPath, { flags: "a", mode: 384 });
6315
7145
  this.redact = createRedactor(env);
6316
7146
  }
6317
7147
  buildId;
@@ -6401,12 +7231,12 @@ function runLoggedCommand(options) {
6401
7231
  let killTimer = null;
6402
7232
  let extraStream;
6403
7233
  if (options.additionalLogPath) {
6404
- extraStream = createWriteStream(options.additionalLogPath, {
7234
+ extraStream = createWriteStream2(options.additionalLogPath, {
6405
7235
  flags: "a",
6406
7236
  mode: 384
6407
7237
  });
6408
7238
  }
6409
- const child = spawn3(options.command, options.args, {
7239
+ const child = spawn5(options.command, options.args, {
6410
7240
  cwd: options.cwd,
6411
7241
  env: options.env,
6412
7242
  shell: false,
@@ -6599,7 +7429,7 @@ function actionArgument(action) {
6599
7429
  }
6600
7430
  function xcodeBuildArguments(input) {
6601
7431
  const resultBundle = join6(input.artifactDirectory, "result.xcresult");
6602
- const sourcePath = containedPath(input.folder, input.source.relativePath);
7432
+ const sourcePath = containedPath2(input.folder, input.source.relativePath);
6603
7433
  const usesCapturedTestProducts = input.action === "TEST_WITHOUT_BUILDING";
6604
7434
  const args = [
6605
7435
  "xcodebuild",
@@ -6646,7 +7476,7 @@ function xcodeBuildSettingsArguments(input, folder = input.folder) {
6646
7476
  "xcodebuild",
6647
7477
  ...sourceArguments(
6648
7478
  input.source,
6649
- containedPath(folder, input.source.relativePath)
7479
+ containedPath2(folder, input.source.relativePath)
6650
7480
  ),
6651
7481
  "-scheme",
6652
7482
  input.scheme,
@@ -6749,7 +7579,7 @@ if (typeof hookModule.default === "function") {
6749
7579
  exitCode: result.exitCode,
6750
7580
  durationMs: Date.now() - started,
6751
7581
  causedBuildFailure: failed && options.script.failureBehavior === "FAIL_BUILD",
6752
- outputRelativePath: relative3(options.input.artifactDirectory, log),
7582
+ outputRelativePath: relative4(options.input.artifactDirectory, log),
6753
7583
  error: failed ? cleanError3(result.output || "Build hook failed") : null
6754
7584
  };
6755
7585
  } catch (hookError) {
@@ -6762,7 +7592,7 @@ if (typeof hookModule.default === "function") {
6762
7592
  exitCode: null,
6763
7593
  durationMs: Date.now() - started,
6764
7594
  causedBuildFailure: !options.signal.aborted && options.script.failureBehavior === "FAIL_BUILD",
6765
- outputRelativePath: relative3(options.input.artifactDirectory, log),
7595
+ outputRelativePath: relative4(options.input.artifactDirectory, log),
6766
7596
  error
6767
7597
  };
6768
7598
  }
@@ -6791,7 +7621,7 @@ async function fileChecksum(path) {
6791
7621
  const information = await stat5(path);
6792
7622
  if (!information.isFile() || information.size > 256 * 1024 * 1024)
6793
7623
  return null;
6794
- return createHash2("sha256").update(await readFile4(path)).digest("hex");
7624
+ return createHash3("sha256").update(await readFile4(path)).digest("hex");
6795
7625
  } catch {
6796
7626
  return null;
6797
7627
  }
@@ -6799,12 +7629,398 @@ async function fileChecksum(path) {
6799
7629
  async function artifact(root, kind, path, metadata = {}) {
6800
7630
  return {
6801
7631
  kind,
6802
- relativePath: relative3(root, path).split(sep2).join("/"),
7632
+ relativePath: relative4(root, path).split(sep3).join("/"),
6803
7633
  sizeBytes: await pathSize(path),
6804
7634
  checksum: await fileChecksum(path),
6805
7635
  metadata
6806
7636
  };
6807
7637
  }
7638
+ function jsonObject(value) {
7639
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
7640
+ }
7641
+ function testDetails(node) {
7642
+ const own = typeof node.details === "string" && node.details.trim() ? [node.details.trim()] : typeof node.name === "string" && ["Failure Message", "Runtime Warning"].includes(String(node.nodeType)) ? [node.name] : [];
7643
+ const children = Array.isArray(node.children) ? node.children : [];
7644
+ return [
7645
+ ...own,
7646
+ ...children.flatMap((child) => {
7647
+ const object = jsonObject(child);
7648
+ return object ? testDetails(object) : [];
7649
+ })
7650
+ ];
7651
+ }
7652
+ function testSourceFile(node, suite) {
7653
+ const explicit = [
7654
+ node.filePath,
7655
+ node.sourceFilePath,
7656
+ node.fileName,
7657
+ node.sourceFile
7658
+ ].find((value) => typeof value === "string" && !!value);
7659
+ if (explicit) {
7660
+ return { file: basename3(explicit), filePath: explicit };
7661
+ }
7662
+ const identifier = typeof node.nodeIdentifier === "string" ? node.nodeIdentifier : "";
7663
+ const identifierOwner = identifier.split("/")[0]?.trim();
7664
+ return {
7665
+ file: identifierOwner || suite,
7666
+ filePath: null
7667
+ };
7668
+ }
7669
+ function normalizeTestResults(value) {
7670
+ const root = jsonObject(value);
7671
+ if (!root || !Array.isArray(root.testNodes)) {
7672
+ throw new Error("xcresulttool returned invalid test results JSON");
7673
+ }
7674
+ const tests = [];
7675
+ const visit = (raw, context) => {
7676
+ const node = jsonObject(raw);
7677
+ if (!node) return;
7678
+ const nodeType = typeof node.nodeType === "string" ? node.nodeType : "";
7679
+ const name = typeof node.name === "string" ? node.name : "";
7680
+ const next = {
7681
+ plan: nodeType === "Test Plan" ? name : context.plan,
7682
+ configuration: nodeType === "Test Plan Configuration" ? name : context.configuration,
7683
+ bundle: /test bundle$/.test(nodeType) ? name : context.bundle,
7684
+ suite: nodeType === "Test Suite" ? name : context.suite
7685
+ };
7686
+ if (nodeType === "Test Case") {
7687
+ const result = typeof node.result === "string" ? node.result : "unknown";
7688
+ const sourceFile = testSourceFile(node, next.suite);
7689
+ tests.push({
7690
+ identifier: typeof node.nodeIdentifier === "string" ? node.nodeIdentifier : name,
7691
+ name,
7692
+ ...next,
7693
+ ...sourceFile,
7694
+ result,
7695
+ durationSeconds: typeof node.durationInSeconds === "number" ? node.durationInSeconds : null,
7696
+ tags: Array.isArray(node.tags) ? node.tags.filter((tag) => typeof tag === "string") : [],
7697
+ details: [...new Set(testDetails(node))]
7698
+ });
7699
+ }
7700
+ if (Array.isArray(node.children)) {
7701
+ for (const child of node.children) visit(child, next);
7702
+ }
7703
+ };
7704
+ for (const node of root.testNodes) {
7705
+ visit(node, {
7706
+ plan: null,
7707
+ configuration: null,
7708
+ bundle: null,
7709
+ suite: null
7710
+ });
7711
+ }
7712
+ const count = (result) => tests.filter((test) => test.result === result).length;
7713
+ return {
7714
+ summary: {
7715
+ total: tests.length,
7716
+ passed: count("Passed"),
7717
+ failed: count("Failed"),
7718
+ skipped: count("Skipped"),
7719
+ expectedFailures: count("Expected Failure"),
7720
+ unknown: tests.filter(
7721
+ (test) => !["Passed", "Failed", "Skipped", "Expected Failure"].includes(
7722
+ String(test.result)
7723
+ )
7724
+ ).length,
7725
+ durationSeconds: tests.reduce(
7726
+ (total, test) => total + Number(test.durationSeconds ?? 0),
7727
+ 0
7728
+ )
7729
+ },
7730
+ data: {
7731
+ devices: Array.isArray(root.devices) ? root.devices : [],
7732
+ configurations: Array.isArray(root.testPlanConfigurations) ? root.testPlanConfigurations : [],
7733
+ tests
7734
+ }
7735
+ };
7736
+ }
7737
+ function normalizeCoverage(value) {
7738
+ const root = jsonObject(value);
7739
+ if (!root || !Array.isArray(root.targets)) {
7740
+ throw new Error("xccov returned invalid coverage JSON");
7741
+ }
7742
+ const files = root.targets.flatMap((rawTarget) => {
7743
+ const target2 = jsonObject(rawTarget);
7744
+ if (!target2 || !Array.isArray(target2.files)) return [];
7745
+ const targetName = typeof target2.name === "string" ? target2.name : "";
7746
+ return target2.files.flatMap((rawFile) => {
7747
+ const file = jsonObject(rawFile);
7748
+ if (!file) return [];
7749
+ return [
7750
+ {
7751
+ target: targetName,
7752
+ name: typeof file.name === "string" ? file.name : "",
7753
+ path: typeof file.path === "string" ? file.path : "",
7754
+ coveredLines: typeof file.coveredLines === "number" ? file.coveredLines : 0,
7755
+ executableLines: typeof file.executableLines === "number" ? file.executableLines : 0,
7756
+ lineCoverage: typeof file.lineCoverage === "number" ? file.lineCoverage : 0
7757
+ }
7758
+ ];
7759
+ });
7760
+ });
7761
+ return {
7762
+ summary: {
7763
+ coveredLines: typeof root.coveredLines === "number" ? root.coveredLines : 0,
7764
+ executableLines: typeof root.executableLines === "number" ? root.executableLines : 0,
7765
+ lineCoverage: typeof root.lineCoverage === "number" ? root.lineCoverage : 0,
7766
+ targetCount: root.targets.length,
7767
+ fileCount: files.length
7768
+ },
7769
+ data: { files }
7770
+ };
7771
+ }
7772
+ async function writeJsonReport(input, kind, timeoutMs, signal) {
7773
+ const resultBundle = join6(input.artifactDirectory, "result.xcresult");
7774
+ const filename = kind === "TEST_RESULTS" ? "test-results.json" : "code-coverage.json";
7775
+ const destination = join6(input.artifactDirectory, filename);
7776
+ const temporary = `${destination}.tmp-${randomUUID2()}`;
7777
+ try {
7778
+ if (!await pathExists(resultBundle)) {
7779
+ throw new Error("The build result bundle is unavailable");
7780
+ }
7781
+ const args = kind === "TEST_RESULTS" ? [
7782
+ "xcresulttool",
7783
+ "get",
7784
+ "test-results",
7785
+ "tests",
7786
+ "--path",
7787
+ "result.xcresult",
7788
+ "--format",
7789
+ "json"
7790
+ ] : ["xccov", "view", "--report", "--json", "result.xcresult"];
7791
+ requireSuccess3(
7792
+ await commandToFile(
7793
+ "xcrun",
7794
+ args,
7795
+ timeoutMs,
7796
+ signal,
7797
+ input.artifactDirectory,
7798
+ xcodeEnvironment(),
7799
+ temporary
7800
+ ),
7801
+ `Could not generate ${filename}`
7802
+ );
7803
+ const serialized = await readFile4(temporary, "utf8");
7804
+ const parsed = JSON.parse(serialized);
7805
+ const normalized = kind === "TEST_RESULTS" ? normalizeTestResults(parsed) : normalizeCoverage(parsed);
7806
+ await rename3(temporary, destination);
7807
+ return {
7808
+ kind,
7809
+ status: "READY",
7810
+ artifact: await artifact(
7811
+ input.artifactDirectory,
7812
+ kind === "TEST_RESULTS" ? "TEST_RESULTS_JSON" : "CODE_COVERAGE_JSON",
7813
+ destination
7814
+ ),
7815
+ ...normalized,
7816
+ error: null
7817
+ };
7818
+ } catch (error) {
7819
+ await rm4(temporary, { force: true });
7820
+ return {
7821
+ kind,
7822
+ status: "FAILED",
7823
+ artifact: null,
7824
+ summary: {},
7825
+ data: {},
7826
+ error: cleanError3(error)
7827
+ };
7828
+ }
7829
+ }
7830
+ async function gitCommand(folder, args, timeoutMs, signal) {
7831
+ return command2("git", ["-C", folder, ...args], timeoutMs, signal, void 0, {
7832
+ ...process.env,
7833
+ GIT_TERMINAL_PROMPT: "0",
7834
+ GIT_OPTIONAL_LOCKS: "0"
7835
+ });
7836
+ }
7837
+ function changedLinesFromPatch(value) {
7838
+ const result = /* @__PURE__ */ new Map();
7839
+ let path = null;
7840
+ for (const line of value.split("\n")) {
7841
+ if (line.startsWith("+++ ")) {
7842
+ const raw = line.slice(4);
7843
+ path = raw === "/dev/null" ? null : raw.replace(/^b\//, "");
7844
+ if (path && !result.has(path)) result.set(path, /* @__PURE__ */ new Set());
7845
+ continue;
7846
+ }
7847
+ if (!path || !line.startsWith("@@")) continue;
7848
+ const match = /\+(\d+)(?:,(\d+))?/.exec(line);
7849
+ if (!match) continue;
7850
+ const start = Number(match[1]);
7851
+ const count = match[2] === void 0 ? 1 : Number(match[2]);
7852
+ const lines = result.get(path);
7853
+ for (let number = start; number < start + count; number += 1) {
7854
+ lines.add(number);
7855
+ }
7856
+ }
7857
+ return result;
7858
+ }
7859
+ function changeTypesFromStatus(value) {
7860
+ const entries = value.split("\0").filter(Boolean);
7861
+ const result = /* @__PURE__ */ new Map();
7862
+ for (let index = 0; index < entries.length; ) {
7863
+ const status2 = entries[index++] ?? "M";
7864
+ const renamed = status2.startsWith("R") || status2.startsWith("C");
7865
+ const first = entries[index++] ?? "";
7866
+ const second = renamed ? entries[index++] ?? "" : null;
7867
+ const path = second ?? first;
7868
+ if (path) result.set(path, status2[0] ?? "M");
7869
+ }
7870
+ return result;
7871
+ }
7872
+ async function snapshotCoverageChanges(input, folder, timeoutMs, signal) {
7873
+ if (!input.baseBranch) throw new Error("A base branch is required");
7874
+ const base = requireSuccess3(
7875
+ await gitCommand(
7876
+ folder,
7877
+ ["merge-base", `refs/remotes/origin/${input.baseBranch}`, "HEAD"],
7878
+ timeoutMs,
7879
+ signal
7880
+ ),
7881
+ "Could not determine the coverage merge base"
7882
+ ).stdout.trim();
7883
+ const [patch, status2, untracked] = await Promise.all([
7884
+ gitCommand(
7885
+ folder,
7886
+ ["diff", "--no-color", "--unified=0", base, "--"],
7887
+ timeoutMs,
7888
+ signal
7889
+ ),
7890
+ gitCommand(
7891
+ folder,
7892
+ ["diff", "--name-status", "-z", base, "--"],
7893
+ timeoutMs,
7894
+ signal
7895
+ ),
7896
+ gitCommand(
7897
+ folder,
7898
+ ["ls-files", "--others", "--exclude-standard", "-z"],
7899
+ timeoutMs,
7900
+ signal
7901
+ )
7902
+ ]);
7903
+ requireSuccess3(patch, "Could not inspect changed coverage lines");
7904
+ requireSuccess3(status2, "Could not inspect coverage change types");
7905
+ requireSuccess3(untracked, "Could not inspect untracked coverage files");
7906
+ const lines = changedLinesFromPatch(patch.stdout);
7907
+ const types = changeTypesFromStatus(status2.stdout);
7908
+ for (const path of untracked.stdout.split("\0").filter(Boolean)) {
7909
+ try {
7910
+ const contents = await readFile4(join6(folder, path));
7911
+ if (contents.includes(0)) continue;
7912
+ const lineCount = contents.length ? contents.toString("utf8").split("\n").length - (contents.toString("utf8").endsWith("\n") ? 1 : 0) : 0;
7913
+ lines.set(
7914
+ path,
7915
+ new Set(Array.from({ length: lineCount }, (_, index) => index + 1))
7916
+ );
7917
+ types.set(path, "A");
7918
+ } catch {
7919
+ }
7920
+ }
7921
+ return [.../* @__PURE__ */ new Set([...lines.keys(), ...types.keys()])].map((path) => ({
7922
+ path,
7923
+ changeType: types.get(path) ?? "M",
7924
+ lines: [...lines.get(path) ?? []].sort((a, b) => a - b)
7925
+ }));
7926
+ }
7927
+ async function addChangedCoverage(report, changes, input, folder, timeoutMs, signal) {
7928
+ if (report.status !== "READY") return report;
7929
+ const rawFiles = Array.isArray(report.data.files) ? report.data.files : [];
7930
+ const coveragePaths = rawFiles.flatMap((raw) => {
7931
+ const file = jsonObject(raw);
7932
+ return file && typeof file.path === "string" ? [file.path] : [];
7933
+ });
7934
+ const changedFiles = [];
7935
+ let changedCoveredLines = 0;
7936
+ let changedExecutableLines = 0;
7937
+ for (const change of changes) {
7938
+ const coveragePath = coveragePaths.find(
7939
+ (candidate) => relative4(folder, candidate).split(sep3).join("/") === change.path
7940
+ );
7941
+ let covered = 0;
7942
+ let executable = 0;
7943
+ if (coveragePath && change.lines.length) {
7944
+ const temporary2 = join6(
7945
+ input.artifactDirectory,
7946
+ `.changed-coverage-${randomUUID2()}.json`
7947
+ );
7948
+ try {
7949
+ const result = await commandToFile(
7950
+ "xcrun",
7951
+ [
7952
+ "xccov",
7953
+ "view",
7954
+ "--archive",
7955
+ "--file",
7956
+ coveragePath,
7957
+ "--json",
7958
+ "result.xcresult"
7959
+ ],
7960
+ timeoutMs,
7961
+ signal,
7962
+ input.artifactDirectory,
7963
+ xcodeEnvironment(),
7964
+ temporary2
7965
+ );
7966
+ if (result.exitCode === 0 && !result.cancelled && !result.timedOut) {
7967
+ const parsed = jsonObject(
7968
+ JSON.parse(await readFile4(temporary2, "utf8"))
7969
+ );
7970
+ const lineData = parsed?.[coveragePath];
7971
+ if (Array.isArray(lineData)) {
7972
+ const changed = new Set(change.lines);
7973
+ for (const rawLine of lineData) {
7974
+ const line = jsonObject(rawLine);
7975
+ if (line?.isExecutable === true && typeof line.line === "number" && changed.has(line.line)) {
7976
+ executable += 1;
7977
+ if (typeof line.executionCount === "number" && line.executionCount > 0) {
7978
+ covered += 1;
7979
+ }
7980
+ }
7981
+ }
7982
+ }
7983
+ }
7984
+ } finally {
7985
+ await rm4(temporary2, { force: true });
7986
+ }
7987
+ }
7988
+ changedCoveredLines += covered;
7989
+ changedExecutableLines += executable;
7990
+ changedFiles.push({
7991
+ path: change.path,
7992
+ changeType: change.changeType,
7993
+ changedCoveredLines: covered,
7994
+ changedExecutableLines: executable,
7995
+ changedLineCoverage: executable ? covered / executable : null
7996
+ });
7997
+ }
7998
+ const summary = {
7999
+ ...report.summary,
8000
+ changedCoveredLines,
8001
+ changedExecutableLines,
8002
+ changedLineCoverage: changedExecutableLines ? changedCoveredLines / changedExecutableLines : null
8003
+ };
8004
+ const data = { ...report.data, changedFiles };
8005
+ const destination = join6(input.artifactDirectory, "worktree-coverage.json");
8006
+ const temporary = `${destination}.tmp-${randomUUID2()}`;
8007
+ await writeFile3(temporary, JSON.stringify({ summary, data }, null, 2), {
8008
+ mode: 384
8009
+ });
8010
+ await rename3(temporary, destination);
8011
+ return {
8012
+ ...report,
8013
+ summary,
8014
+ data,
8015
+ additionalArtifacts: [
8016
+ await artifact(
8017
+ input.artifactDirectory,
8018
+ "WORKTREE_COVERAGE_JSON",
8019
+ destination
8020
+ )
8021
+ ]
8022
+ };
8023
+ }
6808
8024
  function parseBuildSettings(value) {
6809
8025
  const parsed = JSON.parse(value);
6810
8026
  if (!Array.isArray(parsed)) return [];
@@ -6848,14 +8064,35 @@ async function findFiles(root, predicate, limit = 20) {
6848
8064
  }
6849
8065
  return results;
6850
8066
  }
6851
- async function captureArtifacts(input, folder, signal) {
8067
+ async function captureArtifacts(input, folder, signal, includeProducts = true) {
6852
8068
  const artifacts = [];
6853
8069
  const resultBundle = join6(input.artifactDirectory, "result.xcresult");
6854
8070
  if (await pathExists(resultBundle)) {
8071
+ const coverageProbe = await command2(
8072
+ "xcrun",
8073
+ [
8074
+ "xccov",
8075
+ "view",
8076
+ "--report",
8077
+ "--only-targets",
8078
+ "--json",
8079
+ "result.xcresult"
8080
+ ],
8081
+ 3e4,
8082
+ new AbortController().signal,
8083
+ input.artifactDirectory,
8084
+ xcodeEnvironment()
8085
+ );
6855
8086
  artifacts.push(
6856
- await artifact(input.artifactDirectory, "RESULT_BUNDLE", resultBundle)
8087
+ await artifact(input.artifactDirectory, "RESULT_BUNDLE", resultBundle, {
8088
+ testResultsAvailable: ["TEST", "TEST_WITHOUT_BUILDING"].includes(
8089
+ input.action
8090
+ ),
8091
+ coverageAvailable: coverageProbe.exitCode === 0
8092
+ })
6857
8093
  );
6858
8094
  }
8095
+ if (!includeProducts) return artifacts;
6859
8096
  const archivePath = join6(input.artifactDirectory, "archive.xcarchive");
6860
8097
  if (await pathExists(archivePath)) {
6861
8098
  artifacts.push(
@@ -6884,7 +8121,7 @@ async function captureArtifacts(input, folder, signal) {
6884
8121
  const productDirectory = join6(input.artifactDirectory, "products");
6885
8122
  const destination = join6(productDirectory, basename3(source));
6886
8123
  await mkdir3(productDirectory, { recursive: true, mode: 448 });
6887
- await rm3(destination, { recursive: true, force: true });
8124
+ await rm4(destination, { recursive: true, force: true });
6888
8125
  await cp(source, destination, {
6889
8126
  recursive: true,
6890
8127
  preserveTimestamps: true
@@ -6941,9 +8178,14 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
6941
8178
  throw new Error("The captured .xctestrun artifact is unavailable");
6942
8179
  }
6943
8180
  }
6944
- if (!isAbsolute3(input.artifactDirectory) || basename3(input.artifactDirectory) !== input.buildId) {
8181
+ if (!isAbsolute4(input.artifactDirectory) || basename3(input.artifactDirectory) !== input.buildId) {
6945
8182
  throw new Error("Build artifact directory is invalid");
6946
8183
  }
8184
+ const sourceStateHash = await worktreeCodeStateHash(
8185
+ folder,
8186
+ Math.min(timeoutMs, 6e4),
8187
+ signal
8188
+ );
6947
8189
  await mkdir3(input.artifactDirectory, { recursive: true, mode: 448 });
6948
8190
  const rawLog = join6(input.artifactDirectory, "build.log");
6949
8191
  const logger = new BuildLogger(input.buildId, context, rawLog, process.env);
@@ -6966,6 +8208,7 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
6966
8208
  configuration: input.configuration
6967
8209
  };
6968
8210
  let buildResult = null;
8211
+ let coverageChanges = [];
6969
8212
  let errorCode = null;
6970
8213
  let error = null;
6971
8214
  let failBuild = false;
@@ -7023,6 +8266,18 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
7023
8266
  } catch (progressError) {
7024
8267
  logger.emit("RUNNING", "SYSTEM", cleanError3(progressError));
7025
8268
  }
8269
+ if (input.worktreeCoverage) {
8270
+ try {
8271
+ coverageChanges = await snapshotCoverageChanges(
8272
+ input,
8273
+ folder,
8274
+ Math.min(timeoutMs, 6e4),
8275
+ signal
8276
+ );
8277
+ } catch (coverageError) {
8278
+ logger.emit("COVERAGE", "STDERR", cleanError3(coverageError));
8279
+ }
8280
+ }
7026
8281
  const args2 = xcodeBuildArguments(input);
7027
8282
  buildResult = await runLoggedCommand({
7028
8283
  command: "xcrun",
@@ -7082,12 +8337,55 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
7082
8337
  }
7083
8338
  }
7084
8339
  let artifacts = [];
7085
- if (!failBuild && !signal.aborted && buildResult?.exitCode === 0) {
8340
+ try {
8341
+ artifacts = await captureArtifacts(
8342
+ input,
8343
+ folder,
8344
+ new AbortController().signal,
8345
+ !failBuild && !signal.aborted && buildResult?.exitCode === 0
8346
+ );
8347
+ } catch (artifactError) {
8348
+ logger.emit("ARTIFACTS", "STDERR", cleanError3(artifactError));
8349
+ }
8350
+ const reports = [];
8351
+ const testAction = ["TEST", "TEST_WITHOUT_BUILDING"].includes(input.action);
8352
+ const canGenerateReports = () => !signal.aborted && buildResult?.cancelled !== true && buildResult?.timedOut !== true;
8353
+ if (canGenerateReports() && testAction && input.advancedSettings.parseTestResults) {
8354
+ const report = await writeJsonReport(
8355
+ input,
8356
+ "TEST_RESULTS",
8357
+ Math.min(timeoutMs, 12e4),
8358
+ signal
8359
+ );
8360
+ reports.push(report);
8361
+ if (report.artifact) artifacts.push(report.artifact);
8362
+ if (report.error) logger.emit("REPORTS", "STDERR", report.error);
8363
+ }
8364
+ if (canGenerateReports() && testAction && input.worktreeCoverage) {
8365
+ let report = await writeJsonReport(
8366
+ input,
8367
+ "CODE_COVERAGE",
8368
+ Math.min(timeoutMs, 12e4),
8369
+ signal
8370
+ );
7086
8371
  try {
7087
- artifacts = await captureArtifacts(input, folder, signal);
7088
- } catch (artifactError) {
7089
- logger.emit("ARTIFACTS", "STDERR", cleanError3(artifactError));
8372
+ report = await addChangedCoverage(
8373
+ report,
8374
+ coverageChanges,
8375
+ input,
8376
+ folder,
8377
+ Math.min(timeoutMs, 12e4),
8378
+ signal
8379
+ );
8380
+ } catch (coverageError) {
8381
+ logger.emit("COVERAGE", "STDERR", cleanError3(coverageError));
8382
+ }
8383
+ reports.push(report);
8384
+ if (report.artifact) artifacts.push(report.artifact);
8385
+ if (report.additionalArtifacts) {
8386
+ artifacts.push(...report.additionalArtifacts);
7090
8387
  }
8388
+ if (report.error) logger.emit("REPORTS", "STDERR", report.error);
7091
8389
  }
7092
8390
  await logger.close();
7093
8391
  if (await pathExists(rawLog)) {
@@ -7099,6 +8397,12 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
7099
8397
  const timedOut = buildResult?.timedOut === true;
7100
8398
  const exitCode = cancelled ? null : failBuild || !buildResult ? 1 : buildResult.exitCode;
7101
8399
  const args = xcodeBuildArguments(input);
8400
+ const finalStateHash = await worktreeCodeStateHash(
8401
+ folder,
8402
+ Math.min(timeoutMs, 6e4),
8403
+ new AbortController().signal
8404
+ );
8405
+ const codeStateObservedAt = (/* @__PURE__ */ new Date()).toISOString();
7102
8406
  return {
7103
8407
  exitCode,
7104
8408
  signal: buildResult?.signal ?? null,
@@ -7108,16 +8412,44 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
7108
8412
  error,
7109
8413
  commandSummary: commandSummary("xcrun", args),
7110
8414
  artifacts,
7111
- scriptExecutions
8415
+ reports,
8416
+ scriptExecutions,
8417
+ sourceStateHash,
8418
+ finalStateHash,
8419
+ codeStateObservedAt
7112
8420
  };
7113
8421
  } finally {
7114
8422
  await logger.close();
7115
8423
  }
7116
8424
  };
8425
+ var generateIosBuildReport = async (payload, timeoutMs, signal, onLog) => {
8426
+ const input = parseBuildReportPayload(payload);
8427
+ if (!isAbsolute4(input.artifactDirectory) || basename3(input.artifactDirectory) !== input.buildId) {
8428
+ throw new Error("Build artifact directory is invalid");
8429
+ }
8430
+ if (signal.aborted) return { ...successfulProcess5, cancelled: true };
8431
+ const report = await writeJsonReport(
8432
+ input,
8433
+ input.reportKind,
8434
+ Math.min(timeoutMs, 18e4),
8435
+ signal
8436
+ );
8437
+ await onLog({
8438
+ sequence: 0,
8439
+ stream: report.status === "READY" ? "SYSTEM" : "STDERR",
8440
+ message: report.status === "READY" ? `Generated ${report.kind.toLocaleLowerCase()} report` : report.error || "Report generation failed",
8441
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
8442
+ });
8443
+ return {
8444
+ ...successfulProcess5,
8445
+ report,
8446
+ artifacts: report.artifact ? [report.artifact] : []
8447
+ };
8448
+ };
7117
8449
  var deleteIosBuild = async (payload, _timeoutMs, signal, onLog) => {
7118
8450
  const input = parseBuildDeletePayload(payload);
7119
8451
  if (signal.aborted) return { ...successfulProcess5, cancelled: true };
7120
- await rm3(input.artifactDirectory, { recursive: true, force: true });
8452
+ await rm4(input.artifactDirectory, { recursive: true, force: true });
7121
8453
  await onLog({
7122
8454
  sequence: 0,
7123
8455
  stream: "SYSTEM",
@@ -7133,10 +8465,10 @@ var downloadIosBuildArtifact = async (payload, timeoutMs, signal, onLog, context
7133
8465
  }
7134
8466
  const root = await realpath5(input.artifactDirectory);
7135
8467
  const target2 = await realpath5(
7136
- containedPath(root, input.artifactRelativePath)
8468
+ containedPath2(root, input.artifactRelativePath)
7137
8469
  );
7138
- const difference = relative3(root, target2);
7139
- if (!difference || difference === ".." || difference.startsWith(`..${sep2}`) || isAbsolute3(difference)) {
8470
+ const difference = relative4(root, target2);
8471
+ if (!difference || difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute4(difference)) {
7140
8472
  throw new Error("Build artifact resolves outside the build folder");
7141
8473
  }
7142
8474
  const information = await stat5(target2);
@@ -7147,7 +8479,7 @@ var downloadIosBuildArtifact = async (payload, timeoutMs, signal, onLog, context
7147
8479
  try {
7148
8480
  if (information.isDirectory()) {
7149
8481
  temporaryArchive = join6(
7150
- tmpdir(),
8482
+ tmpdir2(),
7151
8483
  `ade-build-artifact-${randomUUID2()}.tar.gz`
7152
8484
  );
7153
8485
  requireSuccess3(
@@ -7180,7 +8512,7 @@ var downloadIosBuildArtifact = async (payload, timeoutMs, signal, onLog, context
7180
8512
  return successfulProcess5;
7181
8513
  } finally {
7182
8514
  if (temporaryArchive) {
7183
- await rm3(temporaryArchive, { force: true });
8515
+ await rm4(temporaryArchive, { force: true });
7184
8516
  }
7185
8517
  }
7186
8518
  };
@@ -7209,7 +8541,7 @@ var deployIosBuild = async (payload, timeoutMs, signal, _onLog, context) => {
7209
8541
  Math.min(timeoutMs, 6e4),
7210
8542
  signal
7211
8543
  );
7212
- const appPath = containedPath(
8544
+ const appPath = containedPath2(
7213
8545
  input.artifactDirectory,
7214
8546
  input.artifactRelativePath
7215
8547
  );
@@ -7387,7 +8719,7 @@ var deployIosBuild = async (payload, timeoutMs, signal, _onLog, context) => {
7387
8719
  status: signal.aborted ? "CANCELLED" : failure ? "FAILED" : "SUCCEEDED",
7388
8720
  error: failure,
7389
8721
  durationMs: Date.now() - started,
7390
- outputRelativePath: relative3(input.artifactDirectory, logPath)
8722
+ outputRelativePath: relative4(input.artifactDirectory, logPath)
7391
8723
  });
7392
8724
  if (signal.aborted) break;
7393
8725
  }
@@ -7447,7 +8779,7 @@ var exportIosArchive = async (payload, timeoutMs, signal, _onLog, context) => {
7447
8779
  Math.min(timeoutMs, 6e4),
7448
8780
  signal
7449
8781
  );
7450
- const archivePath = containedPath(
8782
+ const archivePath = containedPath2(
7451
8783
  input.artifactDirectory,
7452
8784
  input.archiveRelativePath
7453
8785
  );
@@ -7493,7 +8825,7 @@ var exportIosArchive = async (payload, timeoutMs, signal, _onLog, context) => {
7493
8825
  signal: result.signal,
7494
8826
  timedOut: result.timedOut,
7495
8827
  cancelled: result.cancelled,
7496
- outputRelativePath: relative3(input.artifactDirectory, exportDirectory),
8828
+ outputRelativePath: relative4(input.artifactDirectory, exportDirectory),
7497
8829
  sizeBytes: result.exitCode === 0 ? await pathSize(exportDirectory) : null,
7498
8830
  error: result.exitCode === 0 ? null : cleanError3(result.output)
7499
8831
  };
@@ -7522,6 +8854,8 @@ var handlers = {
7522
8854
  [WORKTREE_DELETE_JOB_KIND]: deleteWorktree,
7523
8855
  [WORKTREE_OPERATION_JOB_KIND]: operateWorktree,
7524
8856
  [WORKTREE_WATCH_JOB_KIND]: watchWorktree,
8857
+ [WORKTREE_DIFF_JOB_KIND]: inspectWorktreeDiff,
8858
+ [WORKTREE_DIFF_ASSET_JOB_KIND]: downloadWorktreeDiffAsset,
7525
8859
  [SKILL_SCAN_JOB_KIND]: scanSkills,
7526
8860
  [SKILL_READ_JOB_KIND]: readSkills,
7527
8861
  [SKILL_APPLY_JOB_KIND]: applySkills,
@@ -7533,7 +8867,9 @@ var handlers = {
7533
8867
  [IOS_BUILD_DELETE_JOB_KIND]: deleteIosBuild,
7534
8868
  [IOS_ARTIFACT_DOWNLOAD_JOB_KIND]: downloadIosBuildArtifact,
7535
8869
  [IOS_DEPLOY_JOB_KIND]: deployIosBuild,
7536
- [IOS_EXPORT_JOB_KIND]: exportIosArchive
8870
+ [IOS_EXPORT_JOB_KIND]: exportIosArchive,
8871
+ [IOS_TEST_RESULTS_JOB_KIND]: generateIosBuildReport,
8872
+ [IOS_COVERAGE_REPORT_JOB_KIND]: generateIosBuildReport
7537
8873
  };
7538
8874
 
7539
8875
  // src/repository-coordinator.ts
@@ -7542,8 +8878,8 @@ var RepositoryCoordinator = class {
7542
8878
  async run(key, task) {
7543
8879
  const previous = this.tails.get(key) ?? Promise.resolve();
7544
8880
  let release2;
7545
- const current = new Promise((resolve5) => {
7546
- release2 = resolve5;
8881
+ const current = new Promise((resolve6) => {
8882
+ release2 = resolve6;
7547
8883
  });
7548
8884
  const tail = previous.then(() => current);
7549
8885
  this.tails.set(key, tail);
@@ -7641,7 +8977,7 @@ var JobExecutor = class {
7641
8977
  );
7642
8978
  if (this.stopping) return;
7643
8979
  const delay = Math.min(3e4, 1e3 * 2 ** Math.min(retry++, 5));
7644
- await new Promise((resolve5) => setTimeout(resolve5, delay));
8980
+ await new Promise((resolve6) => setTimeout(resolve6, delay));
7645
8981
  }
7646
8982
  }
7647
8983
  }
@@ -7912,9 +9248,9 @@ async function runAgent(config, signal) {
7912
9248
  () => void heartbeat(),
7913
9249
  HEARTBEAT_INTERVAL_MS
7914
9250
  );
7915
- await new Promise((resolve5) => {
7916
- if (signal.aborted) resolve5();
7917
- else signal.addEventListener("abort", () => resolve5(), { once: true });
9251
+ await new Promise((resolve6) => {
9252
+ if (signal.aborted) resolve6();
9253
+ else signal.addEventListener("abort", () => resolve6(), { once: true });
7918
9254
  });
7919
9255
  clearInterval(heartbeatTimer);
7920
9256
  if (codebaseTimer) clearTimeout(codebaseTimer);
@@ -7931,7 +9267,7 @@ var defaultDependencies = {
7931
9267
  inventory: collectInventory,
7932
9268
  load: loadConfig,
7933
9269
  save: saveConfig,
7934
- wait: (milliseconds, signal) => new Promise((resolve5, reject) => {
9270
+ wait: (milliseconds, signal) => new Promise((resolve6, reject) => {
7935
9271
  if (signal.aborted) {
7936
9272
  reject(new Error("Development agent startup was cancelled"));
7937
9273
  return;
@@ -7942,7 +9278,7 @@ var defaultDependencies = {
7942
9278
  };
7943
9279
  const timer = setTimeout(() => {
7944
9280
  signal.removeEventListener("abort", onAbort);
7945
- resolve5();
9281
+ resolve6();
7946
9282
  }, milliseconds);
7947
9283
  signal.addEventListener("abort", onAbort, { once: true });
7948
9284
  })