hillclimb 0.9.1 → 0.9.3

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.
@@ -160,7 +160,7 @@ async function repoIdentity(repoRoot) {
160
160
  }
161
161
  async function logHeal(level, message) {
162
162
  try {
163
- const { appendLog: appendLog2 } = await import("./log-6OIO3F7C.js");
163
+ const { appendLog: appendLog2 } = await import("./log-UZPPBFMO.js");
164
164
  appendLog2(level, message);
165
165
  } catch {
166
166
  }
@@ -182,7 +182,7 @@ function matchByPath(projects, resolved) {
182
182
  var healMisses = /* @__PURE__ */ new Set();
183
183
  async function healRenamedProject(file, resolved) {
184
184
  const dead = Object.entries(file.projects).filter(
185
- ([key]) => !fs.existsSync(path.resolve(key))
185
+ ([key]) => pathIsGone(path.resolve(key))
186
186
  );
187
187
  if (dead.length === 0) return null;
188
188
  const missKey = `${configPath()}|${resolved}`;
@@ -288,11 +288,26 @@ async function matchLinkedWorktree(projects, resolved) {
288
288
  }
289
289
  return null;
290
290
  }
291
+ function hasNestedGitFile(root, target) {
292
+ let dir = target;
293
+ while (dir !== root && dir.startsWith(root + path.sep)) {
294
+ try {
295
+ if (fs.statSync(path.join(dir, ".git")).isFile()) return true;
296
+ } catch {
297
+ }
298
+ dir = path.dirname(dir);
299
+ }
300
+ return false;
301
+ }
291
302
  async function findProjectForCwd(cwd) {
292
303
  const file = await loadProjects();
293
304
  const resolved = path.resolve(cwd);
294
305
  const match = matchByPath(file.projects, resolved);
295
306
  if (match) {
307
+ if (hasNestedGitFile(match.repoRoot, resolved)) {
308
+ const nested = await matchLinkedWorktree(file.projects, resolved);
309
+ if (nested) return nested;
310
+ }
296
311
  await backfillIdentity(file, match);
297
312
  return { repoRoot: match.repoRoot, config: match.config };
298
313
  }
@@ -383,5 +398,6 @@ export {
383
398
  configPath,
384
399
  loadProjects,
385
400
  upsertProject,
401
+ hasNestedGitFile,
386
402
  findProjectForCwd
387
403
  };
@@ -3,7 +3,7 @@ import {
3
3
  appendLog,
4
4
  setLogPrefix,
5
5
  todayLogPath
6
- } from "./chunk-JZC2F6VA.js";
6
+ } from "./chunk-UPXMA7RC.js";
7
7
  export {
8
8
  appendLog,
9
9
  setLogPrefix,
package/dist/main.js CHANGED
@@ -5,11 +5,12 @@ import {
5
5
  configDir,
6
6
  configPath,
7
7
  findProjectForCwd,
8
+ hasNestedGitFile,
8
9
  loadProjects,
9
10
  setLogPrefix,
10
11
  todayLogPath,
11
12
  upsertProject
12
- } from "./chunk-JZC2F6VA.js";
13
+ } from "./chunk-UPXMA7RC.js";
13
14
 
14
15
  // src/main.ts
15
16
  import fs27 from "fs";
@@ -568,7 +569,7 @@ var PlatformClient = class {
568
569
  const sizeBytes = body.kind === "buffer" ? body.buffer.length : body.sizeBytes;
569
570
  const prefix = `/api/v1/contributions/${encodeURIComponent(contributionId)}`;
570
571
  let digest;
571
- const verify = async (uploadId) => {
572
+ const verify = async (uploadId, options = {}) => {
572
573
  if (digest === void 0) {
573
574
  const hash = createHash("sha256");
574
575
  let localSize = 0;
@@ -590,7 +591,8 @@ var PlatformClient = class {
590
591
  return this.verifyReplayArtifact(
591
592
  `${prefix}/uploads/${encodeURIComponent(uploadId)}/download`,
592
593
  sizeBytes,
593
- digest
594
+ digest,
595
+ options
594
596
  );
595
597
  };
596
598
  const put = async (target2) => {
@@ -642,7 +644,7 @@ var PlatformClient = class {
642
644
  "UPLOAD_REPLAY_MISMATCH"
643
645
  );
644
646
  }
645
- const verified = await verify(existing.id);
647
+ const verified = await verify(existing.id, { forbiddenIsAbsent: true });
646
648
  if (verified.exists) return;
647
649
  target = await this.request(
648
650
  "POST",
@@ -665,7 +667,7 @@ var PlatformClient = class {
665
667
  }
666
668
  await put(target);
667
669
  }
668
- async verifyReplayArtifact(relative, sizeBytes, expectedDigest) {
670
+ async verifyReplayArtifact(relative, sizeBytes, expectedDigest, options = {}) {
669
671
  const redirect = await fetch(this.url(relative), {
670
672
  headers: this.headers(),
671
673
  redirect: "manual",
@@ -708,8 +710,13 @@ var PlatformClient = class {
708
710
  } catch {
709
711
  throw new PlatformError("Stored artifact verification request failed");
710
712
  }
711
- if (response.status === 404) {
713
+ if (response.status === 404 || response.status === 403 && options.forbiddenIsAbsent) {
712
714
  await response.body?.cancel();
715
+ if (response.status === 403)
716
+ appendLog(
717
+ "info",
718
+ "upload-replay: storage refused the pending artifact (403); treating it as never written and letting the guarded PUT decide"
719
+ );
713
720
  return { exists: false, url: location };
714
721
  }
715
722
  if (!response.ok) {
@@ -986,7 +993,7 @@ var TOOLS = [
986
993
  { eventName: "Stop", command: GIT_TRACES_CMD("claude") },
987
994
  { eventName: "SessionEnd", command: GIT_TRACES_CMD("claude") }
988
995
  ],
989
- detect: () => isDir(path4.join(os.homedir(), ".claude"))
996
+ detect: () => isDir(path4.join(homeDir(), ".claude"))
990
997
  },
991
998
  {
992
999
  tool: "cursor",
@@ -1003,7 +1010,7 @@ var TOOLS = [
1003
1010
  { eventName: "stop", command: GIT_TRACES_CMD("cursor") },
1004
1011
  { eventName: "sessionEnd", command: GIT_TRACES_CMD("cursor") }
1005
1012
  ],
1006
- detect: () => isDir(path4.join(os.homedir(), ".cursor"))
1013
+ detect: () => isDir(path4.join(homeDir(), ".cursor"))
1007
1014
  },
1008
1015
  {
1009
1016
  // opencode has no settings-file hook system (the `experimental.hook` block
@@ -1018,7 +1025,7 @@ var TOOLS = [
1018
1025
  format: "opencode",
1019
1026
  events: [],
1020
1027
  // Events are wired inside the plugin itself; see OPENCODE_PLUGIN_CONTENT.
1021
- detect: () => isDir(path4.join(os.homedir(), ".local", "share", "opencode")) || isDir(path4.join(os.homedir(), ".config", "opencode"))
1028
+ detect: () => isDir(path4.join(homeDir(), ".local", "share", "opencode")) || isDir(path4.join(homeDir(), ".config", "opencode"))
1022
1029
  },
1023
1030
  {
1024
1031
  // VS Code Copilot Chat. Same "no SessionEnd" constraint as Codex (the
@@ -1058,7 +1065,7 @@ var TOOLS = [
1058
1065
  { eventName: "SessionStart", command: GIT_TRACES_CMD("codex") },
1059
1066
  { eventName: "Stop", command: GIT_TRACES_CMD("codex") }
1060
1067
  ],
1061
- detect: () => isDir(path4.join(os.homedir(), ".codex"))
1068
+ detect: () => isDir(path4.join(homeDir(), ".codex"))
1062
1069
  }
1063
1070
  ];
1064
1071
  function isDir(p7) {
@@ -1068,8 +1075,11 @@ function isDir(p7) {
1068
1075
  return false;
1069
1076
  }
1070
1077
  }
1078
+ function homeDir() {
1079
+ return process.env.HOME || os.homedir();
1080
+ }
1071
1081
  function copilotChatDetect() {
1072
- const home = os.homedir();
1082
+ const home = homeDir();
1073
1083
  const suffix = path4.join("User", "globalStorage", "github.copilot-chat");
1074
1084
  const candidates = [
1075
1085
  path4.join(home, "Library", "Application Support", "Code", suffix),
@@ -2714,22 +2724,76 @@ import { createReadStream } from "fs";
2714
2724
  import fs11 from "fs/promises";
2715
2725
  import path10 from "path";
2716
2726
 
2727
+ // src/timing-profile.ts
2728
+ var DEFAULT_STAGE_MIN_MS = 250;
2729
+ var DEFAULT_LOCK_WAIT_MIN_MS = 1e3;
2730
+ var STAGE_MIN_MS_ENV = "HILLCLIMB_TIMING_STAGE_MIN_MS";
2731
+ var LOCK_WAIT_MIN_MS_ENV = "HILLCLIMB_TIMING_LOCK_WAIT_MIN_MS";
2732
+ function thresholdMs(name, fallback) {
2733
+ const raw = process.env[name];
2734
+ if (raw === void 0 || raw.trim() === "") return fallback;
2735
+ const value = Number(raw);
2736
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
2737
+ }
2738
+ function shouldLogStage(stage, elapsedMs, status) {
2739
+ if (stage === "lock-wait") {
2740
+ return elapsedMs >= thresholdMs(LOCK_WAIT_MIN_MS_ENV, DEFAULT_LOCK_WAIT_MIN_MS);
2741
+ }
2742
+ return status === "failed" || elapsedMs >= thresholdMs(STAGE_MIN_MS_ENV, DEFAULT_STAGE_MIN_MS);
2743
+ }
2744
+ var StageProfile = class {
2745
+ constructor(prefix) {
2746
+ this.prefix = prefix;
2747
+ }
2748
+ totals = /* @__PURE__ */ new Map();
2749
+ record(stage, elapsedMs, status) {
2750
+ const totals = this.totals.get(stage) ?? {
2751
+ count: 0,
2752
+ totalMs: 0,
2753
+ maxMs: 0,
2754
+ failed: 0
2755
+ };
2756
+ totals.count++;
2757
+ totals.totalMs += elapsedMs;
2758
+ totals.maxMs = Math.max(totals.maxMs, elapsedMs);
2759
+ if (status === "failed") totals.failed++;
2760
+ this.totals.set(stage, totals);
2761
+ }
2762
+ flush() {
2763
+ if (this.totals.size === 0) return;
2764
+ const entries = [...this.totals].map(([stage, totals]) => {
2765
+ let entry = `${stage}=${totals.totalMs}ms/${totals.count}`;
2766
+ if (totals.count > 1) entry += ` max=${totals.maxMs}ms`;
2767
+ if (totals.failed > 0) entry += ` failed=${totals.failed}`;
2768
+ return entry;
2769
+ });
2770
+ appendLog(
2771
+ "info",
2772
+ `${this.prefix} (stages=${this.totals.size}, ${entries.join(", ")})`
2773
+ );
2774
+ this.totals.clear();
2775
+ }
2776
+ };
2777
+
2717
2778
  // src/agent-timing.ts
2779
+ var profile = new StageProfile("agent-upload: timing profile");
2718
2780
  function startAgentStage(stage, context = "") {
2719
2781
  const startedAt = Date.now();
2720
2782
  const started = performance.now();
2721
2783
  const suffix = context ? `, ${context}` : "";
2722
- appendLog(
2723
- "info",
2724
- `agent-upload: timing (stage=${stage}, status=started, startedAt=${startedAt}${suffix})`
2725
- );
2726
2784
  return (status, details) => {
2785
+ const elapsedMs = Math.round(performance.now() - started);
2786
+ profile.record(stage, elapsedMs, status);
2787
+ if (!shouldLogStage(stage, elapsedMs, status)) return;
2727
2788
  appendLog(
2728
2789
  "info",
2729
- `agent-upload: timing (stage=${stage}, status=${status}, elapsedMs=${Math.round(performance.now() - started)}, finishedAt=${Date.now()}${suffix}${details ? `, ${details}` : ""})`
2790
+ `agent-upload: timing (stage=${stage}, status=${status}, elapsedMs=${elapsedMs}, startedAt=${startedAt}, finishedAt=${Date.now()}${suffix}${details ? `, ${details}` : ""})`
2730
2791
  );
2731
2792
  };
2732
2793
  }
2794
+ function flushAgentProfile() {
2795
+ profile.flush();
2796
+ }
2733
2797
  async function measureAgentStage(stage, action, context) {
2734
2798
  const finish = startAgentStage(stage, context);
2735
2799
  let status = "failed";
@@ -3193,6 +3257,47 @@ async function detectCodexLineage(childPath) {
3193
3257
  }
3194
3258
  }
3195
3259
 
3260
+ // src/git-traces/timing.ts
3261
+ import { performance as performance2 } from "perf_hooks";
3262
+ var profile2 = new StageProfile("git-traces: capture profile");
3263
+ function startCaptureStage(stage, context, startedAt = Date.now()) {
3264
+ const started = performance2.now();
3265
+ return (status, details) => {
3266
+ const elapsedMs = Math.round(performance2.now() - started);
3267
+ profile2.record(stage, elapsedMs, status);
3268
+ if (!shouldLogStage(stage, elapsedMs, status)) return;
3269
+ appendLog(
3270
+ "info",
3271
+ `git-traces: capture timing (${context}, stage=${stage}, status=${status}, elapsedMs=${elapsedMs}, startedAt=${startedAt}, finishedAt=${Date.now()}${details ? `, ${details}` : ""})`
3272
+ );
3273
+ };
3274
+ }
3275
+ function flushCaptureProfile() {
3276
+ profile2.flush();
3277
+ }
3278
+ async function timeAsyncCaptureStage(stage, context, run) {
3279
+ const finish = startCaptureStage(stage, context);
3280
+ let status = "failed";
3281
+ try {
3282
+ const result = await run();
3283
+ status = "ok";
3284
+ return result;
3285
+ } finally {
3286
+ finish(status);
3287
+ }
3288
+ }
3289
+ function timeCaptureStage(repoRoot, stage, run) {
3290
+ const finish = startCaptureStage(stage, `repo=${repoRoot}`);
3291
+ let status = "failed";
3292
+ try {
3293
+ const result = run();
3294
+ status = "ok";
3295
+ return result;
3296
+ } finally {
3297
+ finish(status);
3298
+ }
3299
+ }
3300
+
3196
3301
  // src/hook-timing.ts
3197
3302
  var HOOK_RECEIVED_AT_ENV = "HILLCLIMB_HOOK_RECEIVED_AT";
3198
3303
  function pinHookReceivedAt(env, now = Date.now()) {
@@ -3216,6 +3321,8 @@ async function measureHookWork(kind, action) {
3216
3321
  status = "completed";
3217
3322
  return result;
3218
3323
  } finally {
3324
+ flushCaptureProfile();
3325
+ flushAgentProfile();
3219
3326
  appendLog(
3220
3327
  "info",
3221
3328
  `hook-work: kind=${kind} status=${status} finishedAt=${Date.now()} workMs=${Math.round(performance.now() - started)} hookAgeMs=${Math.max(0, Date.now() - receivedAt)}`
@@ -4463,17 +4570,22 @@ async function gitCommonDir(cwd) {
4463
4570
  async function classifyForkCwd(cwd, parentRepoRoot) {
4464
4571
  const resolved = await canonicalPath(cwd);
4465
4572
  const parentRoot = await canonicalPath(parentRepoRoot);
4466
- if (isAtOrBelow(resolved, parentRoot))
4573
+ const below = isAtOrBelow(resolved, parentRoot);
4574
+ if (below && !hasNestedGitFile(parentRoot, resolved))
4467
4575
  return { kind: "same-tree", repoRoot: parentRoot };
4468
4576
  const toplevel = await gitOut(resolved, ["rev-parse", "--show-toplevel"]);
4469
- if (!toplevel) return { kind: "unrelated", repoRoot: null };
4577
+ if (!toplevel)
4578
+ return below ? { kind: "same-tree", repoRoot: parentRoot } : { kind: "unrelated", repoRoot: null };
4579
+ const worktreeRoot = await canonicalPath(toplevel);
4580
+ if (worktreeRoot === parentRoot)
4581
+ return { kind: "same-tree", repoRoot: parentRoot };
4470
4582
  const [forkCommon, parentCommon] = await Promise.all([
4471
4583
  gitCommonDir(resolved),
4472
4584
  gitCommonDir(parentRoot)
4473
4585
  ]);
4474
4586
  if (forkCommon && parentCommon && forkCommon === parentCommon)
4475
- return { kind: "worktree", repoRoot: await canonicalPath(toplevel) };
4476
- return { kind: "unrelated", repoRoot: null };
4587
+ return { kind: "worktree", repoRoot: worktreeRoot };
4588
+ return below ? { kind: "same-tree", repoRoot: parentRoot } : { kind: "unrelated", repoRoot: null };
4477
4589
  }
4478
4590
  async function discoverCodexForkThreads(params) {
4479
4591
  const nowMs = params.nowMs ?? Date.now();
@@ -14656,46 +14768,6 @@ import fs17 from "fs";
14656
14768
  import os7 from "os";
14657
14769
  import path16 from "path";
14658
14770
  import { createGzip } from "zlib";
14659
-
14660
- // src/git-traces/timing.ts
14661
- import { performance as performance2 } from "perf_hooks";
14662
- function startCaptureStage(stage, context, startedAt = Date.now()) {
14663
- const started = performance2.now();
14664
- appendLog(
14665
- "info",
14666
- `git-traces: capture timing (${context}, stage=${stage}, status=started, startedAt=${startedAt})`
14667
- );
14668
- return (status, details) => {
14669
- appendLog(
14670
- "info",
14671
- `git-traces: capture timing (${context}, stage=${stage}, status=${status}, elapsedMs=${Math.round(performance2.now() - started)}, startedAt=${startedAt}, finishedAt=${Date.now()}${details ? `, ${details}` : ""})`
14672
- );
14673
- };
14674
- }
14675
- async function timeAsyncCaptureStage(stage, context, run) {
14676
- const finish = startCaptureStage(stage, context);
14677
- let status = "failed";
14678
- try {
14679
- const result = await run();
14680
- status = "ok";
14681
- return result;
14682
- } finally {
14683
- finish(status);
14684
- }
14685
- }
14686
- function timeCaptureStage(repoRoot, stage, run) {
14687
- const finish = startCaptureStage(stage, `repo=${repoRoot}`);
14688
- let status = "failed";
14689
- try {
14690
- const result = run();
14691
- status = "ok";
14692
- return result;
14693
- } finally {
14694
- finish(status);
14695
- }
14696
- }
14697
-
14698
- // src/git-traces/git-ops.ts
14699
14771
  var DEFAULT_GIT_COMMAND_TIMEOUT_MS = 12e4;
14700
14772
  var DEFAULT_BUNDLE_TIMEOUT_MS = 3e5;
14701
14773
  var FULL_TREE_SCAN_TIMEOUT_MS = 3e5;
@@ -15036,9 +15108,11 @@ function isGitRepo(dir) {
15036
15108
  function stashRetryReason(failure) {
15037
15109
  const status = failure.gitExitStatus;
15038
15110
  if (status !== 1 && status !== 128) return null;
15039
- if (/Unable to create [^\n]*index\.lock[^\n]*: File exists/.test(
15040
- failure.gitStderr ?? ""
15041
- )) {
15111
+ const stderr = failure.gitStderr ?? "";
15112
+ if (/Unable to create [^\n]*index\.lock[^\n]*: File exists/.test(stderr)) {
15113
+ return "index-lock-contention";
15114
+ }
15115
+ if (status === 1 && stderr.trim() === "error: could not write index") {
15042
15116
  return "index-lock-contention";
15043
15117
  }
15044
15118
  if (status === 1 && !failure.gitStderr && !failure.gitStdout) {
@@ -15046,7 +15120,60 @@ function stashRetryReason(failure) {
15046
15120
  }
15047
15121
  return null;
15048
15122
  }
15123
+ var PRIVATE_INDEX_ENV = "HILLCLIMB_GIT_PRIVATE_INDEX";
15124
+ function withPrivateIndex(repoRoot, run) {
15125
+ if (process.env[PRIVATE_INDEX_ENV] === "0") return run(null);
15126
+ let tmpDir = null;
15127
+ let copy = null;
15128
+ try {
15129
+ const indexPath = path16.resolve(
15130
+ repoRoot,
15131
+ git(repoRoot, ["rev-parse", "--git-path", "index"])
15132
+ );
15133
+ tmpDir = fs17.mkdtempSync(path16.join(os7.tmpdir(), "hillclimb-index-"));
15134
+ copy = path16.join(tmpDir, "index");
15135
+ const fd = fs17.openSync(indexPath, "r");
15136
+ let stat;
15137
+ let data;
15138
+ try {
15139
+ stat = fs17.fstatSync(fd);
15140
+ data = fs17.readFileSync(fd);
15141
+ } finally {
15142
+ fs17.closeSync(fd);
15143
+ }
15144
+ fs17.writeFileSync(copy, data);
15145
+ fs17.utimesSync(copy, stat.atime, stat.mtime);
15146
+ } catch (err) {
15147
+ const missing = err.code === "ENOENT";
15148
+ appendLog(
15149
+ missing ? "info" : "warn",
15150
+ `git-traces: private index unavailable; capturing on the repository index (repo=${repoRoot}, reason=${err instanceof Error ? err.message : String(err)})`
15151
+ );
15152
+ copy = null;
15153
+ }
15154
+ try {
15155
+ return run(copy ? privateIndexEnv(copy) : null);
15156
+ } finally {
15157
+ if (tmpDir) fs17.rmSync(tmpDir, { recursive: true, force: true });
15158
+ }
15159
+ }
15160
+ function privateIndexEnv(indexPath) {
15161
+ const existing = Number(process.env.GIT_CONFIG_COUNT);
15162
+ const slot = Number.isInteger(existing) && existing > 0 ? existing : 0;
15163
+ return {
15164
+ GIT_INDEX_FILE: indexPath,
15165
+ GIT_CONFIG_COUNT: String(slot + 1),
15166
+ [`GIT_CONFIG_KEY_${slot}`]: "core.splitIndex",
15167
+ [`GIT_CONFIG_VALUE_${slot}`]: "false"
15168
+ };
15169
+ }
15049
15170
  function captureWorkingCommitSha(repoRoot) {
15171
+ return withPrivateIndex(
15172
+ repoRoot,
15173
+ (indexEnv) => captureWorkingCommitShaOnIndex(repoRoot, indexEnv)
15174
+ );
15175
+ }
15176
+ function captureWorkingCommitShaOnIndex(repoRoot, indexEnv) {
15050
15177
  for (let attempt = 1; ; attempt++) {
15051
15178
  let sha;
15052
15179
  try {
@@ -15057,6 +15184,7 @@ function captureWorkingCommitSha(repoRoot) {
15057
15184
  // Keep the two expected stderr classifications independent of locale.
15058
15185
  gitWithEnv(repoRoot, ["stash", "create"], {
15059
15186
  ...collectorIdentityEnv(),
15187
+ ...indexEnv,
15060
15188
  LC_ALL: "C"
15061
15189
  })
15062
15190
  )
@@ -15069,7 +15197,10 @@ function captureWorkingCommitSha(repoRoot) {
15069
15197
  "info",
15070
15198
  `git-traces: stash create unavailable (repo=${repoRoot}, reason=unborn-head); capturing index tree`
15071
15199
  );
15072
- const tree = git(repoRoot, ["write-tree"]);
15200
+ const tree = gitWithEnv(repoRoot, ["write-tree"], {
15201
+ ...process.env,
15202
+ ...indexEnv
15203
+ });
15073
15204
  return commitTree(repoRoot, tree, "empty baseline");
15074
15205
  }
15075
15206
  const retryReason = stashRetryReason(failure);
@@ -15771,7 +15902,10 @@ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersio
15771
15902
  const headSha = safeGit(repoRoot, ["rev-parse", "HEAD"]) ?? "unknown";
15772
15903
  const branch = safeGit(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? null;
15773
15904
  const remoteUrl = safeGit(repoRoot, ["config", "--get", "remote.origin.url"]) ?? null;
15774
- const statusRaw = safeGitBuffer(repoRoot, ["status", "--porcelain=v1", "-z"]) ?? Buffer.alloc(0);
15905
+ const statusRaw = withPrivateIndex(
15906
+ repoRoot,
15907
+ (indexEnv) => safeGitBuffer(repoRoot, ["status", "--porcelain=v1", "-z"], indexEnv)
15908
+ ) ?? Buffer.alloc(0);
15775
15909
  const dirtyFiles = parseDirtyFilesFromStatus(statusRaw);
15776
15910
  const authorName = safeGit(repoRoot, ["config", "user.name"]) ?? "";
15777
15911
  const authorEmail = safeGit(repoRoot, ["config", "user.email"]) ?? "";
@@ -15920,9 +16054,13 @@ function safeGit(repoRoot, args) {
15920
16054
  return null;
15921
16055
  }
15922
16056
  }
15923
- function safeGitBuffer(repoRoot, args) {
16057
+ function safeGitBuffer(repoRoot, args, env = null) {
15924
16058
  try {
15925
- return gitBuffer(repoRoot, args);
16059
+ return gitBuffer(
16060
+ repoRoot,
16061
+ args,
16062
+ env ? { env: { ...process.env, ...env } } : {}
16063
+ );
15926
16064
  } catch (err) {
15927
16065
  if (isGitTimeoutError(err)) {
15928
16066
  appendLog(
@@ -16831,7 +16969,7 @@ async function uploadArtifact(client, contributionId, filename, mimeType, buffer
16831
16969
  }
16832
16970
 
16833
16971
  // package.json
16834
- var version = "0.9.1";
16972
+ var version = "0.9.3";
16835
16973
 
16836
16974
  // src/version.ts
16837
16975
  var CLI_VERSION = version;
@@ -16972,8 +17110,14 @@ function isPhantomClaudeHook(payload, toolOverride) {
16972
17110
  }
16973
17111
  var MAX_FORK_SWEEP_THREADS = 8;
16974
17112
  async function runUploadInner(payload, options = {}) {
16975
- const outcome = await runUploadForSession(payload, options);
16976
- const settled = outcome === "uploaded" || outcome === "unchanged" || outcome === "skipped";
17113
+ let outcome;
17114
+ let failure;
17115
+ try {
17116
+ outcome = await runUploadForSession(payload, options);
17117
+ } catch (err) {
17118
+ failure = err;
17119
+ }
17120
+ const settled = failure !== void 0 || outcome === "uploaded" || outcome === "unchanged" || outcome === "skipped";
16977
17121
  if (!options.sweptFrom && payload.tool === "codex" && settled) {
16978
17122
  await sweepCodexForkTranscripts(payload);
16979
17123
  }
@@ -16983,6 +17127,7 @@ async function runUploadInner(payload, options = {}) {
16983
17127
  if (!options.sweptFrom && settled) {
16984
17128
  await sweepStrandedAgentUploads(resolveHookSessionId(payload));
16985
17129
  }
17130
+ if (failure !== void 0) throw failure;
16986
17131
  return outcome;
16987
17132
  }
16988
17133
  var MAX_SUBAGENT_SWEEP = 32;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Extract and export AI coding tool logs grouped by repo",
5
5
  "license": "MIT",
6
6
  "type": "module",