hillclimb 0.8.7 → 0.8.9

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/main.js +229 -90
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -14402,6 +14402,35 @@ import fs14 from "fs";
14402
14402
  import os7 from "os";
14403
14403
  import path14 from "path";
14404
14404
  import { createGzip } from "zlib";
14405
+
14406
+ // src/git-traces/timing.ts
14407
+ import { performance } from "perf_hooks";
14408
+ function startCaptureStage(stage, context) {
14409
+ appendLog(
14410
+ "info",
14411
+ `git-traces: capture timing (${context}, stage=${stage}, status=started)`
14412
+ );
14413
+ const started = performance.now();
14414
+ return (status) => {
14415
+ appendLog(
14416
+ "info",
14417
+ `git-traces: capture timing (${context}, stage=${stage}, status=${status}, elapsedMs=${Math.round(performance.now() - started)})`
14418
+ );
14419
+ };
14420
+ }
14421
+ function timeCaptureStage(repoRoot, stage, run) {
14422
+ const finish = startCaptureStage(stage, `repo=${repoRoot}`);
14423
+ let status = "failed";
14424
+ try {
14425
+ const result = run();
14426
+ status = "ok";
14427
+ return result;
14428
+ } finally {
14429
+ finish(status);
14430
+ }
14431
+ }
14432
+
14433
+ // src/git-traces/git-ops.ts
14405
14434
  var DEFAULT_GIT_COMMAND_TIMEOUT_MS = 12e4;
14406
14435
  var FULL_TREE_SCAN_TIMEOUT_MS = 3e5;
14407
14436
  function resolveGitCommandTimeoutMs() {
@@ -14682,6 +14711,7 @@ function createGitError(args, err, timeoutMs = GIT_COMMAND_TIMEOUT_MS) {
14682
14711
  const message = isTimeout ? `${command} timed out after ${timeoutMs}ms` : failure.code === "ENOBUFS" ? `${command} output exceeded the ${formatSize(failure.maxBufferBytes ?? EXEC_OPTS.maxBuffer)} limit` : formatGitFailure(command, failure, err);
14683
14712
  const wrapped = new Error(message);
14684
14713
  wrapped.isGitTimeout = isTimeout;
14714
+ wrapped.gitExitStatus = failure.status;
14685
14715
  const stderr = outputToString(failure.stderr);
14686
14716
  wrapped.gitStderr = stderr ? truncateOutput(stderr) : void 0;
14687
14717
  wrapped.isGitMissingObject = !isTimeout && failure.code !== "ENOBUFS" && matchesMissingObject(stderr);
@@ -14723,20 +14753,50 @@ function isGitRepo(dir) {
14723
14753
  }
14724
14754
  }
14725
14755
  function captureWorkingCommitSha(repoRoot) {
14726
- try {
14727
- const sha = git(repoRoot, ["stash", "create"]);
14728
- if (sha) return sha;
14729
- } catch (err) {
14730
- if (isGitTimeoutError(err)) throw err;
14731
- }
14732
- try {
14733
- return git(repoRoot, ["rev-parse", "HEAD"]);
14734
- } catch (err) {
14735
- if (isGitTimeoutError(err)) throw err;
14736
- const tree = git(repoRoot, ["write-tree"]);
14737
- return git(repoRoot, ["commit-tree", tree, "-m", "empty baseline"]);
14756
+ for (let attempt = 1; ; attempt++) {
14757
+ let sha;
14758
+ try {
14759
+ sha = timeCaptureStage(
14760
+ repoRoot,
14761
+ "stash-create",
14762
+ () => (
14763
+ // Keep the two expected stderr classifications independent of locale.
14764
+ gitWithEnv(repoRoot, ["stash", "create"], {
14765
+ ...process.env,
14766
+ LC_ALL: "C"
14767
+ })
14768
+ )
14769
+ );
14770
+ } catch (err) {
14771
+ const failure = err;
14772
+ const stderr = failure.gitStderr ?? "";
14773
+ if (failure.gitExitStatus === 1 && stderr.includes("You do not have the initial commit yet") && hasUnbornHead(repoRoot)) {
14774
+ appendLog(
14775
+ "info",
14776
+ `git-traces: stash create unavailable (repo=${repoRoot}, reason=unborn-head); capturing index tree`
14777
+ );
14778
+ const tree = git(repoRoot, ["write-tree"]);
14779
+ return git(repoRoot, ["commit-tree", tree, "-m", "empty baseline"]);
14780
+ }
14781
+ if (failure.gitExitStatus !== 1 || !/Unable to create [^\n]*index\.lock[^\n]*: File exists/.test(stderr) || attempt >= 5)
14782
+ throw err;
14783
+ appendLog(
14784
+ "warn",
14785
+ `git-traces: retrying stash create after index lock contention (repo=${repoRoot}, attempt=${attempt}, maxAttempts=5): ${failure.message}`
14786
+ );
14787
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
14788
+ continue;
14789
+ }
14790
+ return sha || git(repoRoot, ["rev-parse", "--verify", "HEAD"]);
14738
14791
  }
14739
14792
  }
14793
+ function hasUnbornHead(repoRoot) {
14794
+ const ref = git(repoRoot, ["symbolic-ref", "--quiet", "HEAD"]);
14795
+ const args = ["show-ref", "--verify", "--quiet", ref];
14796
+ const result = spawnSync("git", args, { cwd: repoRoot, ...EXEC_OPTS });
14797
+ if (result.error) throw createGitError(args, result.error);
14798
+ return result.status === 1;
14799
+ }
14740
14800
  function captureBaselineSha(repoRoot) {
14741
14801
  return captureWorkingCommitSha(repoRoot);
14742
14802
  }
@@ -14971,69 +15031,89 @@ function removePathsFromIndex(repoRoot, env, paths) {
14971
15031
  });
14972
15032
  }
14973
15033
  function filterOmittedFilesFromTree(repoRoot, treeSha, options = {}) {
14974
- const omittedFiles = listOmittedTreeFiles(
15034
+ const omittedFiles = timeCaptureStage(
14975
15035
  repoRoot,
14976
- treeSha,
14977
- options.limits ?? DEFAULT_SNAPSHOT_LIMITS,
14978
- options.previousScan
15036
+ "omission-scan",
15037
+ () => listOmittedTreeFiles(
15038
+ repoRoot,
15039
+ treeSha,
15040
+ options.limits ?? DEFAULT_SNAPSHOT_LIMITS,
15041
+ options.previousScan
15042
+ )
14979
15043
  );
14980
15044
  options.omittedFiles?.push(...omittedFiles);
14981
- if (omittedFiles.length === 0) return treeSha;
14982
- const tmpIndex = path14.join(
14983
- os7.tmpdir(),
14984
- `hillclimb-filter-${Date.now()}-${process.pid}`
14985
- );
14986
- const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
14987
- try {
14988
- gitWithEnv(repoRoot, ["read-tree", treeSha], env);
14989
- removePathsFromIndex(
14990
- repoRoot,
14991
- env,
14992
- omittedFiles.map((file) => file.path)
15045
+ return timeCaptureStage(repoRoot, "tracked-tree-filter", () => {
15046
+ if (omittedFiles.length === 0) return treeSha;
15047
+ const tmpIndex = path14.join(
15048
+ os7.tmpdir(),
15049
+ `hillclimb-filter-${Date.now()}-${process.pid}`
14993
15050
  );
14994
- return gitWithEnv(repoRoot, ["write-tree"], env);
14995
- } finally {
15051
+ const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
14996
15052
  try {
14997
- fs14.unlinkSync(tmpIndex);
14998
- } catch {
15053
+ gitWithEnv(repoRoot, ["read-tree", treeSha], env);
15054
+ removePathsFromIndex(
15055
+ repoRoot,
15056
+ env,
15057
+ omittedFiles.map((file) => file.path)
15058
+ );
15059
+ return gitWithEnv(repoRoot, ["write-tree"], env);
15060
+ } finally {
15061
+ try {
15062
+ fs14.unlinkSync(tmpIndex);
15063
+ } catch {
15064
+ }
14999
15065
  }
15000
- }
15066
+ });
15001
15067
  }
15002
15068
  function buildUntrackedTree(repoRoot, options = {}) {
15003
15069
  const limits = options.limits ?? DEFAULT_SNAPSHOT_LIMITS;
15004
- const list = git(repoRoot, [
15005
- "ls-files",
15006
- "--others",
15007
- "--exclude-standard",
15008
- "-z"
15009
- ]);
15010
- if (!list) return null;
15070
+ const list = timeCaptureStage(
15071
+ repoRoot,
15072
+ "untracked-list",
15073
+ () => git(repoRoot, ["ls-files", "--others", "--exclude-standard", "-z"])
15074
+ );
15011
15075
  const kept = [];
15012
- for (const relPath of list.split("\0")) {
15013
- if (!relPath) continue;
15014
- try {
15015
- const absPath = path14.join(repoRoot, relPath);
15016
- const stat = fs14.lstatSync(absPath);
15017
- const reason = classifyOmission(
15018
- relPath,
15019
- stat.size,
15020
- () => readWorkingFileHead(absPath),
15021
- limits,
15022
- false
15023
- );
15024
- if (reason) {
15025
- options.omittedFiles?.push({
15026
- path: relPath,
15027
- sizeBytes: stat.size,
15028
- tracked: false,
15029
- reason
15030
- });
15031
- continue;
15076
+ let candidates = 0;
15077
+ let keptBytes = 0;
15078
+ let omitted = 0;
15079
+ let omittedBytes = 0;
15080
+ let skipped = 0;
15081
+ timeCaptureStage(repoRoot, "untracked-filter", () => {
15082
+ for (const relPath of list.split("\0")) {
15083
+ if (!relPath) continue;
15084
+ candidates++;
15085
+ try {
15086
+ const absPath = path14.join(repoRoot, relPath);
15087
+ const stat = fs14.lstatSync(absPath);
15088
+ const reason = classifyOmission(
15089
+ relPath,
15090
+ stat.size,
15091
+ () => readWorkingFileHead(absPath),
15092
+ limits,
15093
+ false
15094
+ );
15095
+ if (reason) {
15096
+ omitted++;
15097
+ omittedBytes += stat.size;
15098
+ options.omittedFiles?.push({
15099
+ path: relPath,
15100
+ sizeBytes: stat.size,
15101
+ tracked: false,
15102
+ reason
15103
+ });
15104
+ continue;
15105
+ }
15106
+ kept.push(relPath);
15107
+ keptBytes += stat.size;
15108
+ } catch {
15109
+ skipped++;
15032
15110
  }
15033
- kept.push(relPath);
15034
- } catch {
15035
15111
  }
15036
- }
15112
+ });
15113
+ appendLog(
15114
+ "info",
15115
+ `git-traces: untracked inventory (repo=${repoRoot}, candidates=${candidates}, kept=${kept.length}, keptBytes=${keptBytes}, omitted=${omitted}, omittedBytes=${omittedBytes}, skipped=${skipped})`
15116
+ );
15037
15117
  if (kept.length === 0) return null;
15038
15118
  const tmpIndex = path14.join(
15039
15119
  os7.tmpdir(),
@@ -15041,11 +15121,19 @@ function buildUntrackedTree(repoRoot, options = {}) {
15041
15121
  );
15042
15122
  const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
15043
15123
  try {
15044
- gitBuffer(repoRoot, ["update-index", "--add", "-z", "--stdin"], {
15045
- input: `${kept.join("\0")}\0`,
15046
- env
15047
- });
15048
- return gitWithEnv(repoRoot, ["write-tree"], env);
15124
+ timeCaptureStage(
15125
+ repoRoot,
15126
+ "untracked-hash-and-index",
15127
+ () => gitBuffer(repoRoot, ["update-index", "--add", "-z", "--stdin"], {
15128
+ input: `${kept.join("\0")}\0`,
15129
+ env
15130
+ })
15131
+ );
15132
+ return timeCaptureStage(
15133
+ repoRoot,
15134
+ "untracked-write-tree",
15135
+ () => gitWithEnv(repoRoot, ["write-tree"], env)
15136
+ );
15049
15137
  } finally {
15050
15138
  try {
15051
15139
  fs14.unlinkSync(tmpIndex);
@@ -15056,7 +15144,11 @@ function buildUntrackedTree(repoRoot, options = {}) {
15056
15144
  function buildSnapshotTree(repoRoot, stashSha, options = {}) {
15057
15145
  const omittedFiles = options.omittedFiles ?? [];
15058
15146
  const limits = options.limits ?? DEFAULT_SNAPSHOT_LIMITS;
15059
- const trackedTree = git(repoRoot, ["rev-parse", `${stashSha}^{tree}`]);
15147
+ const trackedTree = timeCaptureStage(
15148
+ repoRoot,
15149
+ "tracked-tree-resolve",
15150
+ () => git(repoRoot, ["rev-parse", `${stashSha}^{tree}`])
15151
+ );
15060
15152
  const trackedOmitted = [];
15061
15153
  const filteredTrackedTree = filterOmittedFilesFromTree(
15062
15154
  repoRoot,
@@ -15068,12 +15160,16 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
15068
15160
  }
15069
15161
  );
15070
15162
  omittedFiles.push(...trackedOmitted);
15071
- const untrackedTree = buildUntrackedTree(repoRoot, { omittedFiles, limits });
15163
+ const untrackedTree = timeCaptureStage(
15164
+ repoRoot,
15165
+ "untracked-capture",
15166
+ () => buildUntrackedTree(repoRoot, { omittedFiles, limits })
15167
+ );
15072
15168
  if (options.log !== false) logOmittedSnapshotSummary(omittedFiles);
15073
- const snapshotTree = mergeSnapshotTrees(
15169
+ const snapshotTree = timeCaptureStage(
15074
15170
  repoRoot,
15075
- filteredTrackedTree,
15076
- untrackedTree
15171
+ "tree-assembly",
15172
+ () => mergeSnapshotTrees(repoRoot, filteredTrackedTree, untrackedTree)
15077
15173
  );
15078
15174
  options.scanOut?.push({
15079
15175
  trackedTreeSha: trackedTree,
@@ -15092,26 +15188,48 @@ function mergeSnapshotTrees(repoRoot, filteredTrackedTree, untrackedTree) {
15092
15188
  );
15093
15189
  const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
15094
15190
  try {
15095
- gitWithEnv(repoRoot, ["read-tree", filteredTrackedTree], env);
15096
- const untrackedList = gitBuffer(
15191
+ timeCaptureStage(
15097
15192
  repoRoot,
15098
- ["ls-tree", "-r", untrackedTree],
15099
- {
15100
- env
15101
- }
15102
- ).toString("utf-8");
15103
- const indexInfo = untrackedList.split("\n").filter(Boolean).map((line) => {
15104
- const [meta, filePath] = line.split(" ");
15105
- const [mode, , sha] = meta.split(/\s+/);
15106
- return `${mode} ${sha} ${filePath}`;
15107
- }).join("\n");
15193
+ "assembly-read-tree",
15194
+ () => gitWithEnv(repoRoot, ["read-tree", filteredTrackedTree], env)
15195
+ );
15196
+ const untrackedList = timeCaptureStage(
15197
+ repoRoot,
15198
+ "assembly-list-untracked",
15199
+ () => gitBuffer(repoRoot, ["ls-tree", "-r", untrackedTree], { env }).toString(
15200
+ "utf-8"
15201
+ )
15202
+ );
15203
+ let overlayEntries = 0;
15204
+ const indexInfo = timeCaptureStage(
15205
+ repoRoot,
15206
+ "assembly-prepare-index",
15207
+ () => untrackedList.split("\n").filter(Boolean).map((line) => {
15208
+ const [meta, filePath] = line.split(" ");
15209
+ const [mode, , sha] = meta.split(/\s+/);
15210
+ overlayEntries++;
15211
+ return `${mode} ${sha} ${filePath}`;
15212
+ }).join("\n")
15213
+ );
15214
+ appendLog(
15215
+ "info",
15216
+ `git-traces: assembly inventory (repo=${repoRoot}, untrackedEntries=${overlayEntries})`
15217
+ );
15108
15218
  if (indexInfo) {
15109
- gitBuffer(repoRoot, ["update-index", "--index-info"], {
15110
- input: indexInfo,
15111
- env
15112
- });
15219
+ timeCaptureStage(
15220
+ repoRoot,
15221
+ "assembly-update-index",
15222
+ () => gitBuffer(repoRoot, ["update-index", "--index-info"], {
15223
+ input: indexInfo,
15224
+ env
15225
+ })
15226
+ );
15113
15227
  }
15114
- return gitWithEnv(repoRoot, ["write-tree"], env);
15228
+ return timeCaptureStage(
15229
+ repoRoot,
15230
+ "assembly-write-tree",
15231
+ () => gitWithEnv(repoRoot, ["write-tree"], env)
15232
+ );
15115
15233
  } finally {
15116
15234
  try {
15117
15235
  fs14.unlinkSync(tmpIndex);
@@ -15709,6 +15827,27 @@ async function deleteLegacySessionState(repoRoot, tool, sessionId) {
15709
15827
  if (legacy?.sessionId === sessionId) await deleteStateFile(legacyFile);
15710
15828
  }
15711
15829
  async function acquireLock3(repoRoot, tool, sessionId, retries, delayMs, options = {}) {
15830
+ const finish = startCaptureStage(
15831
+ "lock-wait",
15832
+ `repo=${repoRoot}, tool=${tool}, session=${sessionId || "<legacy>"}`
15833
+ );
15834
+ let status = "failed";
15835
+ try {
15836
+ const result = await acquireLockWithoutTiming(
15837
+ repoRoot,
15838
+ tool,
15839
+ sessionId,
15840
+ retries,
15841
+ delayMs,
15842
+ options
15843
+ );
15844
+ status = "ok";
15845
+ return result;
15846
+ } finally {
15847
+ finish(status);
15848
+ }
15849
+ }
15850
+ async function acquireLockWithoutTiming(repoRoot, tool, sessionId, retries, delayMs, options = {}) {
15712
15851
  const lockPath = lockFileForRepo(repoRoot, tool, sessionId);
15713
15852
  await fs15.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
15714
15853
  const bounded = retries !== void 0 || delayMs !== void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
4
4
  "description": "Extract and export AI coding tool logs grouped by repo",
5
5
  "license": "MIT",
6
6
  "type": "module",