hillclimb 0.5.0 → 0.5.2

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/cli.js +634 -260
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2,10 +2,11 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import fs19 from "fs";
5
- import path23 from "path";
5
+ import path24 from "path";
6
6
  import * as p6 from "@clack/prompts";
7
7
 
8
8
  // src/commands/init.ts
9
+ import path7 from "path";
9
10
  import * as p3 from "@clack/prompts";
10
11
 
11
12
  // src/color.ts
@@ -124,67 +125,18 @@ function detectRepoRoot() {
124
125
  }
125
126
  }
126
127
 
127
- // src/gitignore.ts
128
+ // src/identity.ts
128
129
  import fs2 from "fs";
129
130
  import path3 from "path";
130
- function toGitignorePattern(repoRoot, file) {
131
- const rel = path3.relative(repoRoot, file);
132
- if (!rel || rel.startsWith("..") || path3.isAbsolute(rel)) return null;
133
- return `/${rel.split(path3.sep).join("/")}`;
134
- }
135
- function normalizePattern(pattern) {
136
- return pattern.trim().replace(/\\/g, "/").replace(/^\/+/, "");
137
- }
138
- async function ensureGitignored(repoRoot, files) {
139
- const gitignorePath = path3.join(repoRoot, ".gitignore");
140
- const patterns = [
141
- ...new Set(
142
- files.map((file) => toGitignorePattern(repoRoot, file)).filter((pattern) => pattern !== null)
143
- )
144
- ];
145
- let content = "";
146
- let created = false;
147
- try {
148
- content = await fs2.promises.readFile(gitignorePath, "utf-8");
149
- } catch (err) {
150
- if (err.code !== "ENOENT") throw err;
151
- created = true;
152
- }
153
- const existing = new Set(
154
- content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map(normalizePattern)
155
- );
156
- const added = patterns.filter(
157
- (pattern) => !existing.has(normalizePattern(pattern))
158
- );
159
- if (added.length === 0) {
160
- return { path: gitignorePath, added: [], created: false, changed: false };
161
- }
162
- let next = content;
163
- if (next && !next.endsWith("\n")) next += "\n";
164
- if (next) next += "\n";
165
- next += ["# Hillclimb hook files", ...added].join("\n");
166
- next += "\n";
167
- await fs2.promises.writeFile(gitignorePath, next);
168
- return {
169
- path: gitignorePath,
170
- added,
171
- created,
172
- changed: true
173
- };
174
- }
175
-
176
- // src/identity.ts
177
- import fs3 from "fs";
178
- import path4 from "path";
179
131
  function identityPath() {
180
- return path4.join(configDir(), "identity.json");
132
+ return path3.join(configDir(), "identity.json");
181
133
  }
182
134
  function normalizeUrl(apiBaseUrl) {
183
135
  return apiBaseUrl.replace(/\/$/, "");
184
136
  }
185
137
  async function loadAllIdentities() {
186
138
  try {
187
- const raw = await fs3.promises.readFile(identityPath(), "utf-8");
139
+ const raw = await fs2.promises.readFile(identityPath(), "utf-8");
188
140
  const parsed = JSON.parse(raw);
189
141
  if (!parsed.identities || typeof parsed.identities !== "object") {
190
142
  return { identities: {} };
@@ -199,12 +151,12 @@ async function loadIdentity(apiBaseUrl) {
199
151
  return file.identities[normalizeUrl(apiBaseUrl)] ?? null;
200
152
  }
201
153
  async function writeIdentityFile(file) {
202
- await fs3.promises.mkdir(configDir(), { recursive: true, mode: 448 });
154
+ await fs2.promises.mkdir(configDir(), { recursive: true, mode: 448 });
203
155
  const tmp = `${identityPath()}.tmp`;
204
- await fs3.promises.writeFile(tmp, JSON.stringify(file, null, 2), {
156
+ await fs2.promises.writeFile(tmp, JSON.stringify(file, null, 2), {
205
157
  mode: 384
206
158
  });
207
- await fs3.promises.rename(tmp, identityPath());
159
+ await fs2.promises.rename(tmp, identityPath());
208
160
  }
209
161
  async function saveIdentity(identity) {
210
162
  const file = await loadAllIdentities();
@@ -242,8 +194,8 @@ function formatError(err) {
242
194
  }
243
195
 
244
196
  // src/platform/log.ts
245
- import fs4 from "fs";
246
- import path5 from "path";
197
+ import fs3 from "fs";
198
+ import path4 from "path";
247
199
  var PT_TIME_ZONE = "America/Los_Angeles";
248
200
  function pacificParts(date) {
249
201
  const parts = new Intl.DateTimeFormat("en-US", {
@@ -294,10 +246,10 @@ function pacificDateString(date) {
294
246
  return `${p7.year}-${p7.month}-${p7.day}`;
295
247
  }
296
248
  function logsDir() {
297
- return path5.join(configDir(), "logs");
249
+ return path4.join(configDir(), "logs");
298
250
  }
299
251
  function todayLogPath() {
300
- return path5.join(logsDir(), `${pacificDateString(/* @__PURE__ */ new Date())}.log`);
252
+ return path4.join(logsDir(), `${pacificDateString(/* @__PURE__ */ new Date())}.log`);
301
253
  }
302
254
  var logPrefix = "";
303
255
  function setLogPrefix(prefix) {
@@ -308,8 +260,8 @@ function appendLog(level, message) {
308
260
  const line = `[${pacificTimestamp(/* @__PURE__ */ new Date())}] [${level}]${tag} ${message}
309
261
  `;
310
262
  try {
311
- fs4.mkdirSync(logsDir(), { recursive: true, mode: 448 });
312
- fs4.appendFileSync(todayLogPath(), line);
263
+ fs3.mkdirSync(logsDir(), { recursive: true, mode: 448 });
264
+ fs3.appendFileSync(todayLogPath(), line);
313
265
  } catch {
314
266
  process.stderr.write(`hillclimb:${tag} ${level}: ${message}
315
267
  `);
@@ -597,9 +549,61 @@ var PlatformClient = class {
597
549
  };
598
550
 
599
551
  // src/platform/hooks.ts
552
+ import { execFileSync } from "child_process";
600
553
  import fs5 from "fs";
601
554
  import os2 from "os";
602
555
  import path6 from "path";
556
+
557
+ // src/gitignore.ts
558
+ import fs4 from "fs";
559
+ import path5 from "path";
560
+ function toGitignorePattern(repoRoot, file) {
561
+ const rel = path5.relative(repoRoot, file);
562
+ if (!rel || rel.startsWith("..") || path5.isAbsolute(rel)) return null;
563
+ return `/${rel.split(path5.sep).join("/")}`;
564
+ }
565
+ function normalizePattern(pattern) {
566
+ return pattern.trim().replace(/\\/g, "/").replace(/^\/+/, "");
567
+ }
568
+ async function ensureGitignored(repoRoot, files) {
569
+ const gitignorePath = path5.join(repoRoot, ".gitignore");
570
+ const patterns = [
571
+ ...new Set(
572
+ files.map((file) => toGitignorePattern(repoRoot, file)).filter((pattern) => pattern !== null)
573
+ )
574
+ ];
575
+ let content = "";
576
+ let created = false;
577
+ try {
578
+ content = await fs4.promises.readFile(gitignorePath, "utf-8");
579
+ } catch (err) {
580
+ if (err.code !== "ENOENT") throw err;
581
+ created = true;
582
+ }
583
+ const existing = new Set(
584
+ content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map(normalizePattern)
585
+ );
586
+ const added = patterns.filter(
587
+ (pattern) => !existing.has(normalizePattern(pattern))
588
+ );
589
+ if (added.length === 0) {
590
+ return { path: gitignorePath, added: [], created: false, changed: false };
591
+ }
592
+ let next = content;
593
+ if (next && !next.endsWith("\n")) next += "\n";
594
+ if (next) next += "\n";
595
+ next += ["# Hillclimb hook files", ...added].join("\n");
596
+ next += "\n";
597
+ await fs4.promises.writeFile(gitignorePath, next);
598
+ return {
599
+ path: gitignorePath,
600
+ added,
601
+ created,
602
+ changed: true
603
+ };
604
+ }
605
+
606
+ // src/platform/hooks.ts
603
607
  var HOOK_CMD = (sub) => `npx hillclimb@latest ${sub}`;
604
608
  var GIT_TRACES_CMD = (tool) => `npx hillclimb@latest git-traces --tool=${tool}`;
605
609
  var CLAUDE_SESSIONEND_UPLOAD_TIMEOUT_SECONDS = 30;
@@ -723,20 +727,41 @@ function copilotChatDetect() {
723
727
  return candidates.some(isDir);
724
728
  }
725
729
  function ensureHooks(settings) {
726
- if (!settings.hooks) settings.hooks = {};
730
+ if (!settings.hooks || typeof settings.hooks !== "object" || Array.isArray(settings.hooks)) {
731
+ settings.hooks = {};
732
+ }
727
733
  return settings.hooks;
728
734
  }
729
735
  function settingsPath(repoRoot, def) {
730
736
  return path6.join(repoRoot, def.settingsFile);
731
737
  }
732
- async function readJson(file) {
738
+ async function readJson(file, options = {}) {
733
739
  try {
734
740
  const raw = await fs5.promises.readFile(file, "utf-8");
741
+ if (!raw.trim()) return {};
735
742
  const parsed = JSON.parse(raw);
736
743
  if (parsed && typeof parsed === "object") return parsed;
737
744
  return {};
738
745
  } catch (err) {
739
746
  if (err.code === "ENOENT") return {};
747
+ if (options.repairMalformed && err instanceof SyntaxError) {
748
+ const backup = `${file}.malformed-${Date.now()}`;
749
+ try {
750
+ const raw = await fs5.promises.readFile(file, "utf-8");
751
+ await fs5.promises.writeFile(backup, raw);
752
+ appendLog(
753
+ "warn",
754
+ `hooks: backed up malformed JSON hook file ${file} to ${backup}`
755
+ );
756
+ } catch (backupErr) {
757
+ appendLog(
758
+ "warn",
759
+ `hooks: could not back up malformed JSON hook file ${file}: ${backupErr instanceof Error ? backupErr.message : String(backupErr)}`
760
+ );
761
+ }
762
+ return {};
763
+ }
764
+ if (err instanceof SyntaxError) return {};
740
765
  throw err;
741
766
  }
742
767
  }
@@ -1167,6 +1192,19 @@ function legacyBareGitTracesCommandsFor(command) {
1167
1192
  (legacy) => legacy.endsWith(" git-traces")
1168
1193
  );
1169
1194
  }
1195
+ function legacyBareUploadCommandsFor(command) {
1196
+ const prefix = "npx hillclimb@latest ";
1197
+ if (!command.startsWith(prefix)) return [];
1198
+ const sub = command.slice(prefix.length);
1199
+ if (!sub.startsWith("upload --tool=")) return [];
1200
+ return [
1201
+ "npx hillclimb@latest upload",
1202
+ "npx hillclimb upload",
1203
+ "hillclimb-extract upload",
1204
+ "npx hillclimb-extract upload",
1205
+ "npx @hillclimb/extract upload"
1206
+ ];
1207
+ }
1170
1208
  function isHillclimbOwnedCommandFor(command, currentCommand) {
1171
1209
  if (command === currentCommand || legacyCommandsFor(currentCommand).includes(command)) {
1172
1210
  return true;
@@ -1182,7 +1220,7 @@ async function installHooksForTool(repoRoot, def) {
1182
1220
  const r = await opencodeInstall(file);
1183
1221
  return { settingsFile: file, ...r, changed: r.installed > 0 };
1184
1222
  }
1185
- const settings = await readJson(file);
1223
+ const settings = await readJson(file, { repairMalformed: true });
1186
1224
  let installed = 0;
1187
1225
  let alreadyPresent = 0;
1188
1226
  let mutated = false;
@@ -1227,6 +1265,10 @@ async function checkHooksForTool(repoRoot, def) {
1227
1265
  missing.push(`${evt.eventName}:${evt.command}`);
1228
1266
  }
1229
1267
  }
1268
+ if (def.tool === "codex" && missing.length === 0) {
1269
+ const enabled = await areCodexHooksEnabled();
1270
+ if (!enabled) missing.push("features.hooks");
1271
+ }
1230
1272
  return { allInstalled: missing.length === 0, settingsFile: file, missing };
1231
1273
  }
1232
1274
  async function findLegacyGitTracesHookOwners(repoRoot, eventName) {
@@ -1250,9 +1292,57 @@ async function findLegacyGitTracesHookOwners(repoRoot, eventName) {
1250
1292
  }
1251
1293
  return owners;
1252
1294
  }
1295
+ async function findLegacyUploadHookOwners(repoRoot, eventName) {
1296
+ if (!eventName) return [];
1297
+ const owners = [];
1298
+ for (const def of TOOLS) {
1299
+ if (def.format === "opencode") continue;
1300
+ const events = def.events.filter(
1301
+ (evt) => evt.eventName === eventName && evt.command.startsWith("npx hillclimb@latest upload")
1302
+ );
1303
+ if (events.length === 0) continue;
1304
+ const settings = await readJson(settingsPath(repoRoot, def));
1305
+ const commands = new Set(commandsForEvent(settings, def.format, eventName));
1306
+ if (events.some((evt) => {
1307
+ const candidates = evt.command.includes("--tool=") ? legacyBareUploadCommandsFor(evt.command) : [evt.command, ...legacyCommandsFor(evt.command)];
1308
+ return candidates.some((candidate) => commands.has(candidate));
1309
+ })) {
1310
+ owners.push(def.tool);
1311
+ }
1312
+ }
1313
+ return owners;
1314
+ }
1253
1315
  function detectTools(repoRoot) {
1254
1316
  return TOOLS.filter((def) => def.detect(repoRoot));
1255
1317
  }
1318
+ function trackedHookFiles(repoRoot, files) {
1319
+ const rels = files.map((file) => path6.relative(repoRoot, file)).filter((rel) => rel && !rel.startsWith("..") && !path6.isAbsolute(rel));
1320
+ if (rels.length === 0) return [];
1321
+ try {
1322
+ const output = execFileSync("git", ["ls-files", "--", ...rels], {
1323
+ cwd: repoRoot,
1324
+ stdio: ["ignore", "pipe", "ignore"]
1325
+ }).toString("utf-8").trim();
1326
+ if (!output) return [];
1327
+ return output.split(/\r?\n/).map((rel) => path6.join(repoRoot, rel));
1328
+ } catch {
1329
+ return [];
1330
+ }
1331
+ }
1332
+ async function ensureHookFilesIgnored(repoRoot, files) {
1333
+ const ignored = await ensureGitignored(repoRoot, [
1334
+ ...files,
1335
+ ...files.map((file) => `${file}.malformed-*`)
1336
+ ]);
1337
+ const tracked = trackedHookFiles(repoRoot, files);
1338
+ if (tracked.length > 0) {
1339
+ appendLog(
1340
+ "warn",
1341
+ `hooks: hook files are already tracked by git and may be committed (${tracked.join(", ")})`
1342
+ );
1343
+ }
1344
+ return { ...ignored, tracked };
1345
+ }
1256
1346
  async function installDetectedHooks(repoRoot) {
1257
1347
  const detected = detectTools(repoRoot);
1258
1348
  const results = [];
@@ -1280,6 +1370,20 @@ async function healHookForTool(repoRoot, tool) {
1280
1370
  };
1281
1371
  }
1282
1372
  const r = await installHooksForTool(repoRoot, def);
1373
+ try {
1374
+ const ignored = await ensureHookFilesIgnored(repoRoot, [r.settingsFile]);
1375
+ if (ignored.changed) {
1376
+ appendLog(
1377
+ "info",
1378
+ `self-heal: gitignored ${def.tool} hook file (${ignored.added.join(",")})`
1379
+ );
1380
+ }
1381
+ } catch (err) {
1382
+ appendLog(
1383
+ "warn",
1384
+ `self-heal: could not update .gitignore for ${def.tool} hook: ${err instanceof Error ? err.message : String(err)}`
1385
+ );
1386
+ }
1283
1387
  return {
1284
1388
  tool: def.tool,
1285
1389
  label: def.label,
@@ -1303,20 +1407,38 @@ async function checkAllHooks(repoRoot) {
1303
1407
  }
1304
1408
  return results;
1305
1409
  }
1306
- var CODEX_CONFIG_PATH = path6.join(os2.homedir(), ".codex", "config.toml");
1410
+ function codexConfigPath() {
1411
+ return process.env.HILLCLIMB_CODEX_CONFIG_PATH ?? path6.join(os2.homedir(), ".codex", "config.toml");
1412
+ }
1413
+ function codexHooksEnabledInConfig(content) {
1414
+ const featuresMatch = content.match(/^\[features\]\s*$/m);
1415
+ if (!featuresMatch || featuresMatch.index === void 0) return false;
1416
+ const bodyStart = featuresMatch.index + featuresMatch[0].length;
1417
+ const nextSectionOffset = content.slice(bodyStart).search(/\n\[[^\n]+\]\s*(?:\r?\n|$)/);
1418
+ const bodyEnd = nextSectionOffset === -1 ? content.length : bodyStart + nextSectionOffset;
1419
+ const body = content.slice(bodyStart, bodyEnd);
1420
+ const hooksMatch = body.match(/^\s*hooks\s*=\s*(true|false)\s*(?:#.*)?$/im);
1421
+ return hooksMatch?.[1]?.toLowerCase() === "true";
1422
+ }
1423
+ async function areCodexHooksEnabled() {
1424
+ try {
1425
+ const content = await fs5.promises.readFile(codexConfigPath(), "utf-8");
1426
+ return codexHooksEnabledInConfig(content);
1427
+ } catch {
1428
+ return false;
1429
+ }
1430
+ }
1307
1431
  async function ensureCodexHooksEnabled() {
1308
1432
  let content;
1433
+ const configPath2 = codexConfigPath();
1309
1434
  try {
1310
- content = await fs5.promises.readFile(CODEX_CONFIG_PATH, "utf-8");
1435
+ content = await fs5.promises.readFile(configPath2, "utf-8");
1311
1436
  } catch (err) {
1312
1437
  if (err.code === "ENOENT") {
1313
- await fs5.promises.mkdir(path6.dirname(CODEX_CONFIG_PATH), {
1438
+ await fs5.promises.mkdir(path6.dirname(configPath2), {
1314
1439
  recursive: true
1315
1440
  });
1316
- await fs5.promises.writeFile(
1317
- CODEX_CONFIG_PATH,
1318
- "[features]\nhooks = true\n"
1319
- );
1441
+ await fs5.promises.writeFile(configPath2, "[features]\nhooks = true\n");
1320
1442
  return true;
1321
1443
  }
1322
1444
  throw err;
@@ -1354,7 +1476,7 @@ hooks = true
1354
1476
  `;
1355
1477
  }
1356
1478
  if (content === original) return false;
1357
- await fs5.promises.writeFile(CODEX_CONFIG_PATH, content);
1479
+ await fs5.promises.writeFile(configPath2, content);
1358
1480
  return true;
1359
1481
  }
1360
1482
  var CLAUDE_DEF = TOOLS[0];
@@ -1754,6 +1876,7 @@ async function runInit(args = []) {
1754
1876
  userId: bootstrap.session.userId,
1755
1877
  savedAt: (/* @__PURE__ */ new Date()).toISOString()
1756
1878
  });
1879
+ const autoUploadWarnings = [];
1757
1880
  try {
1758
1881
  const hookResults = await installDetectedHooks(repoRoot);
1759
1882
  if (hookResults.length === 0) {
@@ -1778,7 +1901,7 @@ async function runInit(args = []) {
1778
1901
  }
1779
1902
  }
1780
1903
  try {
1781
- const ignored = await ensureGitignored(
1904
+ const ignored = await ensureHookFilesIgnored(
1782
1905
  repoRoot,
1783
1906
  hookResults.map((r) => r.settingsFile)
1784
1907
  );
@@ -1791,11 +1914,21 @@ async function runInit(args = []) {
1791
1914
  `${ignored.created ? "Created" : "Updated"} .gitignore for hook files`
1792
1915
  );
1793
1916
  }
1917
+ if (ignored.tracked.length > 0) {
1918
+ const tracked = ignored.tracked.map((file) => path7.relative(repoRoot, file)).join(", ");
1919
+ autoUploadWarnings.push(
1920
+ `Hook files are already tracked by git: ${tracked}`
1921
+ );
1922
+ p3.log.warn(
1923
+ `Hook files are already tracked by git and may be committed: ${tracked}`
1924
+ );
1925
+ }
1794
1926
  } catch (err) {
1795
1927
  appendLog(
1796
1928
  "warn",
1797
1929
  `init: could not update .gitignore for hook files: ${formatError(err)}`
1798
1930
  );
1931
+ autoUploadWarnings.push("Could not update .gitignore for hook files.");
1799
1932
  p3.log.warn(
1800
1933
  `Could not update .gitignore for hook files: ${err instanceof Error ? err.message : String(err)}`
1801
1934
  );
@@ -1812,6 +1945,9 @@ async function runInit(args = []) {
1812
1945
  "warn",
1813
1946
  `init: could not enable Codex hooks: ${formatError(err)}`
1814
1947
  );
1948
+ autoUploadWarnings.push(
1949
+ "Could not enable Codex hooks in config.toml."
1950
+ );
1815
1951
  p3.log.warn(
1816
1952
  `Could not enable hooks in config.toml: ${err instanceof Error ? err.message : String(err)}`
1817
1953
  );
@@ -1831,7 +1967,7 @@ async function runInit(args = []) {
1831
1967
  `init: completed (project=${project.slug}, contributionType=${type.slug})`
1832
1968
  );
1833
1969
  p3.outro(
1834
- `Done. Your next coding session in this repo will upload automatically to ${project.name}.`
1970
+ autoUploadWarnings.length > 0 ? `Configured ${project.name}, but auto-upload needs attention: ${autoUploadWarnings.join(" ")}` : `Done. Your next coding session in this repo will upload automatically to ${project.name}.`
1835
1971
  );
1836
1972
  }
1837
1973
 
@@ -2050,7 +2186,7 @@ async function runLogout(args = []) {
2050
2186
  }
2051
2187
 
2052
2188
  // src/commands/status.ts
2053
- import path7 from "path";
2189
+ import path8 from "path";
2054
2190
  async function runStatus(args = []) {
2055
2191
  const debug = args.includes("--debug");
2056
2192
  header("status");
@@ -2068,7 +2204,7 @@ async function runStatus(args = []) {
2068
2204
  return;
2069
2205
  }
2070
2206
  const { repoRoot, config } = match;
2071
- const repoName = repo?.name ?? path7.basename(repoRoot);
2207
+ const repoName = repo?.name ?? path8.basename(repoRoot);
2072
2208
  if (IS_TTY) {
2073
2209
  process.stdout.write(` ${dim("Checking login...")}`);
2074
2210
  }
@@ -2162,7 +2298,7 @@ async function runStatus(args = []) {
2162
2298
  try {
2163
2299
  const hookStatuses = await checkAllHooks(repoRoot);
2164
2300
  for (const h of hookStatuses) {
2165
- const rel = path7.relative(repoRoot, h.settingsFile) || h.settingsFile;
2301
+ const rel = path8.relative(repoRoot, h.settingsFile) || h.settingsFile;
2166
2302
  debugRow(
2167
2303
  `${h.label} hook:`,
2168
2304
  `${rel} ${dim(`(${h.installed ? "installed" : "missing"})`)}`
@@ -2179,14 +2315,16 @@ async function runStatus(args = []) {
2179
2315
 
2180
2316
  // src/commands/upload.ts
2181
2317
  import { spawn as spawn2 } from "child_process";
2318
+ import crypto3 from "crypto";
2182
2319
  import fs11 from "fs";
2183
2320
  import os6 from "os";
2184
- import path14 from "path";
2321
+ import path15 from "path";
2322
+ import readline from "readline";
2185
2323
 
2186
2324
  // src/debug-logs.ts
2187
2325
  import crypto from "crypto";
2188
2326
  import fs9 from "fs";
2189
- import path11 from "path";
2327
+ import path12 from "path";
2190
2328
 
2191
2329
  // src/hook-events.ts
2192
2330
  function classifyHookEvent(event) {
@@ -2205,6 +2343,41 @@ function classifyHookEvent(event) {
2205
2343
  }
2206
2344
  }
2207
2345
 
2346
+ // src/hook-payload.ts
2347
+ function resolveHookCwd(payload) {
2348
+ return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
2349
+ }
2350
+ function resolveHookSessionId(payload) {
2351
+ return payload.session_id ?? payload.conversation_id ?? null;
2352
+ }
2353
+ function inferSourceToolFromPayload(payload) {
2354
+ if (payload.tool) return payload.tool;
2355
+ if (payload.cursor_version) return "cursor";
2356
+ const transcriptPath = payload.transcript_path ?? "";
2357
+ if (transcriptPath.includes(".claude/projects/")) return "claude";
2358
+ if (transcriptPath.includes(".cursor/projects/")) return "cursor";
2359
+ if (transcriptPath.includes("GitHub.copilot-chat")) return "copilot-chat";
2360
+ if (transcriptPath.includes(".hillclimb/opencode-transcripts/")) {
2361
+ return "opencode";
2362
+ }
2363
+ if (transcriptPath.includes(".codex/")) return "codex";
2364
+ switch (payload.hook_event_name) {
2365
+ case "session.idle":
2366
+ case "session.deleted":
2367
+ case "server.instance.disposed":
2368
+ return "opencode";
2369
+ default:
2370
+ return null;
2371
+ }
2372
+ }
2373
+ function fallbackSourceTool(payload) {
2374
+ const inferred = inferSourceToolFromPayload(payload);
2375
+ if (inferred) return inferred;
2376
+ const eventKind = classifyHookEvent(payload.hook_event_name);
2377
+ if (eventKind === "stop") return "codex";
2378
+ return "claude";
2379
+ }
2380
+
2208
2381
  // src/middleware/pattern-redact.ts
2209
2382
  import os3 from "os";
2210
2383
  import { Worker } from "worker_threads";
@@ -10692,7 +10865,7 @@ var middleware = [];
10692
10865
 
10693
10866
  // src/middleware/secrets.ts
10694
10867
  import fs7 from "fs";
10695
- import path8 from "path";
10868
+ import path9 from "path";
10696
10869
  var KNOWN_NON_SECRETS = /* @__PURE__ */ new Set([
10697
10870
  "true",
10698
10871
  "false",
@@ -10847,7 +11020,7 @@ async function discoverEnvFiles(repoRoot) {
10847
11020
  const envFiles = [];
10848
11021
  for (const name of entries) {
10849
11022
  if (!name.startsWith(".env")) continue;
10850
- const filePath = path8.join(repoRoot, name);
11023
+ const filePath = path9.join(repoRoot, name);
10851
11024
  try {
10852
11025
  const stat = await fs7.promises.stat(filePath);
10853
11026
  if (stat.isFile()) envFiles.push(name);
@@ -10872,7 +11045,7 @@ async function collectSecrets(repoRoot, envFiles, additionalFiles) {
10872
11045
  }
10873
11046
  }
10874
11047
  for (const filePath of additionalFiles) {
10875
- const resolved = path8.resolve(repoRoot, filePath);
11048
+ const resolved = path9.resolve(repoRoot, filePath);
10876
11049
  sourceFiles.push(resolved);
10877
11050
  for (const value of await parseEnvFile(resolved)) {
10878
11051
  if (isUsableValue(value)) {
@@ -10902,16 +11075,16 @@ import archiver from "archiver";
10902
11075
 
10903
11076
  // src/outputs/archive.ts
10904
11077
  import os4 from "os";
10905
- import path9 from "path";
11078
+ import path10 from "path";
10906
11079
  function getSourceBaseDir(sourceName) {
10907
11080
  const home = os4.homedir();
10908
11081
  switch (sourceName) {
10909
11082
  case "claude":
10910
- return path9.join(home, ".claude", "projects");
11083
+ return path10.join(home, ".claude", "projects");
10911
11084
  case "codex":
10912
- return path9.join(home, ".codex", "sessions");
11085
+ return path10.join(home, ".codex", "sessions");
10913
11086
  case "debug-logs":
10914
- return path9.join(configDir(), "logs");
11087
+ return path10.join(configDir(), "logs");
10915
11088
  default:
10916
11089
  return home;
10917
11090
  }
@@ -10920,8 +11093,8 @@ function addGroupToArchive(archive, group, selectedSources) {
10920
11093
  for (const file of group.files) {
10921
11094
  if (!selectedSources.has(file.sourceName)) continue;
10922
11095
  const baseDir = getSourceBaseDir(file.sourceName);
10923
- const relativePath = file.absolutePath.startsWith(baseDir) ? path9.relative(baseDir, file.absolutePath) : path9.basename(file.absolutePath);
10924
- const archivePath = path9.join(file.sourceName, relativePath);
11096
+ const relativePath = file.absolutePath.startsWith(baseDir) ? path10.relative(baseDir, file.absolutePath) : path10.basename(file.absolutePath);
11097
+ const archivePath = path10.join(file.sourceName, relativePath);
10925
11098
  if (file.content) {
10926
11099
  archive.append(file.content, { name: archivePath });
10927
11100
  } else {
@@ -11005,20 +11178,20 @@ var PlatformUploadOutput = class {
11005
11178
 
11006
11179
  // src/pipeline.ts
11007
11180
  import fs8 from "fs";
11008
- import path10 from "path";
11181
+ import path11 from "path";
11009
11182
  function canonicalizePath(p7) {
11010
- let resolved = path10.resolve(p7);
11011
- if (resolved.endsWith(path10.sep) && resolved !== path10.sep) {
11183
+ let resolved = path11.resolve(p7);
11184
+ if (resolved.endsWith(path11.sep) && resolved !== path11.sep) {
11012
11185
  resolved = resolved.slice(0, -1);
11013
11186
  }
11014
11187
  return resolved;
11015
11188
  }
11016
11189
  function computeLabel(repoPath, allPaths) {
11017
- const segments = repoPath.split(path10.sep).filter(Boolean);
11190
+ const segments = repoPath.split(path11.sep).filter(Boolean);
11018
11191
  for (let depth = 1; depth <= segments.length; depth++) {
11019
11192
  const label = segments.slice(-depth).join("/");
11020
11193
  const matches = allPaths.filter((p7) => {
11021
- const s = p7.split(path10.sep).filter(Boolean);
11194
+ const s = p7.split(path11.sep).filter(Boolean);
11022
11195
  return s.slice(-depth).join("/") === label;
11023
11196
  });
11024
11197
  if (matches.length === 1) return label;
@@ -11096,7 +11269,7 @@ var DEFAULT_WAIT_MS = 6e4;
11096
11269
  var LOCK_RETRIES = 100;
11097
11270
  var LOCK_RETRY_DELAY_MS = 100;
11098
11271
  function stateDir() {
11099
- return process.env.HILLCLIMB_DEBUG_LOG_STATE_DIR ?? path11.join(configDir(), "debug-log-uploads");
11272
+ return process.env.HILLCLIMB_DEBUG_LOG_STATE_DIR ?? path12.join(configDir(), "debug-log-uploads");
11100
11273
  }
11101
11274
  function waitMs() {
11102
11275
  const raw = process.env.HILLCLIMB_DEBUG_LOG_WAIT_MS;
@@ -11105,7 +11278,7 @@ function waitMs() {
11105
11278
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_WAIT_MS;
11106
11279
  }
11107
11280
  function stateFile(eventId) {
11108
- return path11.join(stateDir(), `${eventId}.json`);
11281
+ return path12.join(stateDir(), `${eventId}.json`);
11109
11282
  }
11110
11283
  function lockFile(eventId) {
11111
11284
  return `${stateFile(eventId)}.lock`;
@@ -11130,10 +11303,10 @@ function toolLabel(tool) {
11130
11303
  return labels[tool] ?? tool;
11131
11304
  }
11132
11305
  function resolveCwd(payload) {
11133
- return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
11306
+ return resolveHookCwd(payload);
11134
11307
  }
11135
11308
  function resolveSessionId(payload) {
11136
- return payload.session_id ?? payload.conversation_id ?? null;
11309
+ return resolveHookSessionId(payload);
11137
11310
  }
11138
11311
  function stringOrNull(value) {
11139
11312
  return typeof value === "string" && value.length > 0 ? value : null;
@@ -11152,7 +11325,7 @@ function expectedKinds(tool, eventKind, payload) {
11152
11325
  async function transcriptFingerprint(payload) {
11153
11326
  const transcriptPath = stringOrNull(payload.transcript_path);
11154
11327
  if (!transcriptPath) return {};
11155
- const resolved = path11.resolve(transcriptPath);
11328
+ const resolved = path12.resolve(transcriptPath);
11156
11329
  try {
11157
11330
  const stat = await fs9.promises.stat(resolved);
11158
11331
  return {
@@ -11191,13 +11364,10 @@ async function eventContext(tool, payload) {
11191
11364
  conversationId: stringOrNull(payload.conversation_id),
11192
11365
  turnId,
11193
11366
  eventNonce,
11194
- // Only per-turn stop events need the transcript to disambiguate (they
11195
- // often lack a turn id); sessionEnd is keyed by sessionId. Folding it in
11196
- // for sessionEnd would diverge the agent/git eventIds whenever the two
11197
- // hooks send different payloads e.g. the opencode plugin passes
11198
- // transcript_path to the upload spawn but not the git-traces spawn,
11199
- // causing a 60s orphan wait and a duplicate debug-log upload.
11200
- ...eventKind === "stop" ? await transcriptFingerprint(payload) : {}
11367
+ // Only stop events without stable turn IDs need the transcript to
11368
+ // disambiguate. When a turn_id exists, including mutable transcript
11369
+ // mtime/size can split agent+git completions for the same logical event.
11370
+ ...eventKind === "stop" && !turnId ? await transcriptFingerprint(payload) : {}
11201
11371
  };
11202
11372
  const eventId = crypto.createHash("sha256").update(JSON.stringify(fingerprint)).digest("hex").slice(0, 32);
11203
11373
  return {
@@ -11268,7 +11438,7 @@ function initialState(ctx, now) {
11268
11438
  eventKind: ctx.eventKind,
11269
11439
  hookEventName: ctx.hookEventName,
11270
11440
  sessionId: ctx.sessionId,
11271
- logDate: path11.basename(todayLogPath(), ".log"),
11441
+ logDate: path12.basename(todayLogPath(), ".log"),
11272
11442
  firstSeenAt: now.toISOString()
11273
11443
  };
11274
11444
  }
@@ -11330,7 +11500,7 @@ async function waitForExpectedKinds(ctx, state) {
11330
11500
  }
11331
11501
  async function buildMiddleware(repoRoot) {
11332
11502
  const envFileNames = await discoverEnvFiles(repoRoot);
11333
- const envFilePaths = envFileNames.map((n) => path11.join(repoRoot, n));
11503
+ const envFilePaths = envFileNames.map((n) => path12.join(repoRoot, n));
11334
11504
  const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
11335
11505
  const middleware2 = [];
11336
11506
  if (secretResult.values.size > 0) {
@@ -11376,7 +11546,7 @@ async function uploadDebugLog(ctx, state) {
11376
11546
  };
11377
11547
  const group = {
11378
11548
  repoPath: ctx.repoRoot,
11379
- label: path11.basename(ctx.repoRoot),
11549
+ label: path12.basename(ctx.repoRoot),
11380
11550
  files: [sourceFile],
11381
11551
  sourceNames: [DEBUG_LOGS_SLUG],
11382
11552
  lastModified: now
@@ -11398,7 +11568,7 @@ async function uploadDebugLog(ctx, state) {
11398
11568
  `Tool: ${label}`,
11399
11569
  `Event: ${ctx.hookEventName ?? ctx.eventKind}`,
11400
11570
  `Repo: ${ctx.repoRoot}`,
11401
- `Log: ${path11.basename(logPath)}`,
11571
+ `Log: ${path12.basename(logPath)}`,
11402
11572
  `Agent done: ${state.agentDoneAt ?? "<not observed>"}`,
11403
11573
  `Git done: ${state.gitDoneAt ?? "<not observed>"}`,
11404
11574
  `Uploaded: ${now.toISOString()}`
@@ -11415,7 +11585,7 @@ async function uploadDebugLog(ctx, state) {
11415
11585
  );
11416
11586
  appendLog(
11417
11587
  "info",
11418
- `debug-logs: uploaded ${path11.basename(logPath)} to project ${ctx.config.projectSlug} (${ctx.config.projectId}) as contribution ${contributionId}`
11588
+ `debug-logs: uploaded ${path12.basename(logPath)} to project ${ctx.config.projectSlug} (${ctx.config.projectId}) as contribution ${contributionId}`
11419
11589
  );
11420
11590
  return contributionId;
11421
11591
  } catch (err) {
@@ -11468,7 +11638,7 @@ async function recordDebugLogCompletion(args) {
11468
11638
  }
11469
11639
 
11470
11640
  // src/normalizer/index.ts
11471
- import path12 from "path";
11641
+ import path13 from "path";
11472
11642
 
11473
11643
  // src/normalizer/claude.ts
11474
11644
  function stringify(value) {
@@ -13166,14 +13336,20 @@ var NormalizeMiddleware = class {
13166
13336
  continue;
13167
13337
  const content = file.content ? file.content.toString("utf-8") : null;
13168
13338
  if (!content) continue;
13169
- const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 : path12.basename(file.absolutePath, ".jsonl"));
13339
+ const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 : path13.basename(file.absolutePath, ".jsonl"));
13170
13340
  try {
13171
13341
  const trajectory = normalizeContent(
13172
13342
  file.sourceName,
13173
13343
  content,
13174
13344
  sessionId
13175
13345
  );
13176
- if (!trajectory) continue;
13346
+ if (!trajectory) {
13347
+ appendLog(
13348
+ "warn",
13349
+ `normalize: produced no ATIF for source=${file.sourceName} session=${sessionId ?? "<unknown>"} file=${file.absolutePath}`
13350
+ );
13351
+ continue;
13352
+ }
13177
13353
  const json = JSON.stringify(
13178
13354
  excludeNone(trajectory),
13179
13355
  null,
@@ -13187,7 +13363,11 @@ var NormalizeMiddleware = class {
13187
13363
  metadata: { ...file.metadata, isAtif: true },
13188
13364
  content: Buffer.from(json, "utf-8")
13189
13365
  });
13190
- } catch {
13366
+ } catch (err) {
13367
+ appendLog(
13368
+ "warn",
13369
+ `normalize: failed for source=${file.sourceName} session=${sessionId ?? "<unknown>"} file=${file.absolutePath}: ${err instanceof Error ? err.message : String(err)}`
13370
+ );
13191
13371
  }
13192
13372
  }
13193
13373
  return { ...group, files: newFiles };
@@ -13198,15 +13378,15 @@ var NormalizeMiddleware = class {
13198
13378
  import crypto2 from "crypto";
13199
13379
  import fs10 from "fs";
13200
13380
  import os5 from "os";
13201
- import path13 from "path";
13381
+ import path14 from "path";
13202
13382
  var CURRENT_SCHEMA_VERSION2 = 1;
13203
- var DEFAULT_STATE_DIR = path13.join(
13383
+ var DEFAULT_STATE_DIR = path14.join(
13204
13384
  os5.homedir(),
13205
13385
  ".hillclimb",
13206
13386
  "agent-uploads"
13207
13387
  );
13208
- var LOCK_RETRIES2 = 120;
13209
- var LOCK_RETRY_DELAY_MS2 = 500;
13388
+ var DEFAULT_LOCK_WAIT_MS = 5 * 60 * 1e3;
13389
+ var DEFAULT_LOCK_RETRY_DELAY_MS = 500;
13210
13390
  var DEFAULT_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
13211
13391
  function stateDir2() {
13212
13392
  return process.env.HILLCLIMB_UPLOAD_STATE_DIR ?? DEFAULT_STATE_DIR;
@@ -13223,9 +13403,26 @@ function stateTtlMs() {
13223
13403
  DEFAULT_STATE_TTL_MS
13224
13404
  );
13225
13405
  }
13406
+ function lockRetryDelayMs() {
13407
+ return Math.max(
13408
+ 1,
13409
+ readPositiveEnvMs(
13410
+ "HILLCLIMB_UPLOAD_LOCK_RETRY_DELAY_MS",
13411
+ DEFAULT_LOCK_RETRY_DELAY_MS
13412
+ )
13413
+ );
13414
+ }
13415
+ function lockRetries() {
13416
+ return Math.max(
13417
+ 1,
13418
+ Math.ceil(
13419
+ readPositiveEnvMs("HILLCLIMB_UPLOAD_LOCK_WAIT_MS", DEFAULT_LOCK_WAIT_MS) / lockRetryDelayMs()
13420
+ )
13421
+ );
13422
+ }
13226
13423
  function stateFileFor(repoRoot, tool, sessionId) {
13227
- const hash = crypto2.createHash("sha256").update(`${path13.resolve(repoRoot)}\0${tool}\0${sessionId}`).digest("hex").slice(0, 16);
13228
- return path13.join(stateDir2(), `${hash}.json`);
13424
+ const hash = crypto2.createHash("sha256").update(`${path14.resolve(repoRoot)}\0${tool}\0${sessionId}`).digest("hex").slice(0, 16);
13425
+ return path14.join(stateDir2(), `${hash}.json`);
13229
13426
  }
13230
13427
  function lockFileFor(repoRoot, tool, sessionId) {
13231
13428
  return `${stateFileFor(repoRoot, tool, sessionId)}.lock`;
@@ -13258,7 +13455,7 @@ async function deleteUploadState(repoRoot, tool, sessionId) {
13258
13455
  } catch {
13259
13456
  }
13260
13457
  }
13261
- async function acquireLock2(repoRoot, tool, sessionId, retries = LOCK_RETRIES2, delayMs = LOCK_RETRY_DELAY_MS2) {
13458
+ async function acquireLock2(repoRoot, tool, sessionId, retries = lockRetries(), delayMs = lockRetryDelayMs()) {
13262
13459
  const lockPath = lockFileFor(repoRoot, tool, sessionId);
13263
13460
  await fs10.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
13264
13461
  for (let i = 0; i < retries; i++) {
@@ -13267,18 +13464,24 @@ async function acquireLock2(repoRoot, tool, sessionId, retries = LOCK_RETRIES2,
13267
13464
  lockPath,
13268
13465
  fs10.constants.O_CREAT | fs10.constants.O_EXCL | fs10.constants.O_WRONLY
13269
13466
  );
13270
- await fd.write(String(process.pid));
13271
- await fd.close();
13467
+ try {
13468
+ await fd.write(String(process.pid));
13469
+ } finally {
13470
+ await fd.close();
13471
+ }
13272
13472
  return;
13273
13473
  } catch (err) {
13274
13474
  if (err.code === "EEXIST" && i < retries - 1) {
13275
13475
  await new Promise((r) => setTimeout(r, delayMs));
13276
13476
  continue;
13277
13477
  }
13478
+ if (err.code === "EEXIST") break;
13278
13479
  throw err;
13279
13480
  }
13280
13481
  }
13281
- throw new Error(`Failed to acquire upload lock after ${retries} retries`);
13482
+ throw new Error(
13483
+ `Failed to acquire upload lock for ${tool} session ${sessionId} after ${retries} retries (${delayMs}ms delay, lock=${lockPath})`
13484
+ );
13282
13485
  }
13283
13486
  async function releaseLock2(repoRoot, tool, sessionId) {
13284
13487
  try {
@@ -13303,7 +13506,7 @@ async function sweepStaleUploadStates(ttlMs = stateTtlMs(), now = Date.now()) {
13303
13506
  }
13304
13507
  for (const entry of entries) {
13305
13508
  if (!entry.isFile()) continue;
13306
- const file = path13.join(stateDir2(), entry.name);
13509
+ const file = path14.join(stateDir2(), entry.name);
13307
13510
  try {
13308
13511
  const st = await fs10.promises.stat(file);
13309
13512
  if (now - st.mtimeMs > ttlMs) {
@@ -13329,6 +13532,9 @@ function sanitize2(value) {
13329
13532
  function formatEpochSeconds2(date) {
13330
13533
  return String(Math.floor(date.getTime() / 1e3));
13331
13534
  }
13535
+ function newFlowId() {
13536
+ return crypto3.randomBytes(3).toString("hex");
13537
+ }
13332
13538
  function lineHasAssistant(line) {
13333
13539
  return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
13334
13540
  }
@@ -13355,16 +13561,41 @@ async function hasAssistantMessage(transcriptPath) {
13355
13561
  }
13356
13562
  return false;
13357
13563
  }
13358
- function resolveSourceTool(payload) {
13359
- if (payload.tool) return payload.tool;
13360
- if (payload.cursor_version) return "cursor";
13361
- if (payload.hook_event_name === "Stop") return "codex";
13362
- return "claude";
13564
+ function hashFileSha256(file) {
13565
+ return new Promise((resolve, reject) => {
13566
+ const hash = crypto3.createHash("sha256");
13567
+ const stream = fs11.createReadStream(file);
13568
+ stream.on("data", (chunk) => hash.update(chunk));
13569
+ stream.on("error", reject);
13570
+ stream.on("end", () => resolve(hash.digest("hex")));
13571
+ });
13572
+ }
13573
+ async function resolveSourceTool(payload, repoRoot) {
13574
+ const inferred = inferSourceToolFromPayload(payload);
13575
+ if (inferred) return inferred;
13576
+ if (repoRoot) {
13577
+ const owners = await findLegacyUploadHookOwners(
13578
+ repoRoot,
13579
+ payload.hook_event_name
13580
+ );
13581
+ if (owners.length === 1) {
13582
+ appendLog("info", `upload: inferred legacy bare hook owner ${owners[0]}`);
13583
+ return owners[0];
13584
+ }
13585
+ if (owners.length > 1) {
13586
+ appendLog(
13587
+ "warn",
13588
+ `upload: legacy bare hook matched multiple tools (${owners.join(", ")}); skipping to avoid misattribution`
13589
+ );
13590
+ return null;
13591
+ }
13592
+ }
13593
+ return fallbackSourceTool(payload);
13363
13594
  }
13364
13595
  function summarizePayload(payload) {
13365
- const sessionId = payload.session_id ?? payload.conversation_id ?? null;
13596
+ const sessionId = resolveHookSessionId(payload);
13366
13597
  const turnId = payload.turn_id ?? null;
13367
- const cwd = payload.cwd ?? payload.workspace_roots?.[0] ?? null;
13598
+ const cwd = resolveHookCwd(payload);
13368
13599
  return JSON.stringify({
13369
13600
  session_id: sessionId,
13370
13601
  turn_id: turnId,
@@ -13378,6 +13609,54 @@ function summarizePayload(payload) {
13378
13609
  cursor_version_present: !!payload.cursor_version
13379
13610
  });
13380
13611
  }
13612
+ async function codexSessionIdFromFile(filePath) {
13613
+ const stream = fs11.createReadStream(filePath, { encoding: "utf-8" });
13614
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
13615
+ try {
13616
+ for await (const line of rl) {
13617
+ if (!line.trim()) continue;
13618
+ try {
13619
+ const obj = JSON.parse(line);
13620
+ if (obj.type === "session_meta") return obj.payload?.id ?? null;
13621
+ } catch {
13622
+ return null;
13623
+ }
13624
+ return null;
13625
+ }
13626
+ } finally {
13627
+ rl.close();
13628
+ stream.destroy();
13629
+ }
13630
+ return null;
13631
+ }
13632
+ async function findCodexTranscriptPath(sessionId) {
13633
+ const sessionsDir = process.env.HILLCLIMB_CODEX_SESSIONS_DIR ?? path15.join(os6.homedir(), ".codex", "sessions");
13634
+ const candidates = [];
13635
+ async function walk(dir) {
13636
+ let entries;
13637
+ try {
13638
+ entries = await fs11.promises.readdir(dir, { withFileTypes: true });
13639
+ } catch {
13640
+ return;
13641
+ }
13642
+ for (const entry of entries) {
13643
+ const full = path15.join(dir, entry.name);
13644
+ if (entry.isDirectory()) {
13645
+ await walk(full);
13646
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.includes(sessionId)) {
13647
+ candidates.push(full);
13648
+ }
13649
+ }
13650
+ }
13651
+ await walk(sessionsDir);
13652
+ candidates.sort();
13653
+ for (const filePath of candidates) {
13654
+ if (await codexSessionIdFromFile(filePath) === sessionId) {
13655
+ return filePath;
13656
+ }
13657
+ }
13658
+ return void 0;
13659
+ }
13381
13660
  async function selfHealHook(repoRoot, tool) {
13382
13661
  if (process.env.HILLCLIMB_SKIP_HOOK_SELF_HEAL === "1") return;
13383
13662
  try {
@@ -13397,11 +13676,11 @@ async function selfHealHook(repoRoot, tool) {
13397
13676
  }
13398
13677
  }
13399
13678
  function resolveCursorTranscriptPath(payload) {
13400
- const id = payload.conversation_id ?? payload.session_id;
13679
+ const id = resolveHookSessionId(payload);
13401
13680
  const workspace = payload.workspace_roots?.[0];
13402
13681
  if (!id || !workspace) return void 0;
13403
13682
  const encoded = workspace.replace(/^\//, "").replace(/\//g, "-");
13404
- return path14.join(
13683
+ return path15.join(
13405
13684
  os6.homedir(),
13406
13685
  ".cursor",
13407
13686
  "projects",
@@ -13411,22 +13690,31 @@ function resolveCursorTranscriptPath(payload) {
13411
13690
  `${id}.jsonl`
13412
13691
  );
13413
13692
  }
13693
+ async function resolveTranscriptPath(payload, sourceTool, sessionId) {
13694
+ if (payload.transcript_path) return payload.transcript_path;
13695
+ if (sourceTool === "cursor") return resolveCursorTranscriptPath(payload);
13696
+ if (sourceTool === "codex") {
13697
+ const resolved = await findCodexTranscriptPath(sessionId);
13698
+ if (resolved) {
13699
+ appendLog(
13700
+ "info",
13701
+ `[${sessionId}] resolved missing Codex transcript_path via ~/.codex/sessions: ${resolved}`
13702
+ );
13703
+ }
13704
+ return resolved;
13705
+ }
13706
+ return void 0;
13707
+ }
13414
13708
  async function runUploadInner(payload) {
13415
- const sessionId = payload.session_id;
13416
- const transcriptPath = payload.transcript_path ?? resolveCursorTranscriptPath(payload);
13417
- const cwd = payload.cwd ?? payload.workspace_roots?.[0];
13418
- const sourceTool = resolveSourceTool(payload);
13709
+ const sessionId = resolveHookSessionId(payload);
13710
+ const cwd = resolveHookCwd(payload);
13419
13711
  const eventKind = classifyHookEvent(payload.hook_event_name);
13420
- appendLog(
13421
- "info",
13422
- `[${sessionId ?? "no-id"}] payload parsed (tool=${sourceTool}, cwd=${cwd ?? "?"}, event=${eventKind ?? "?"})`
13423
- );
13424
- if (!sessionId || !transcriptPath || !cwd) {
13712
+ if (!sessionId || !cwd) {
13425
13713
  appendLog(
13426
13714
  "warn",
13427
- `Skipping upload: missing required hook fields (session_id=${!!sessionId}, transcript_path=${!!transcriptPath}, cwd=${!!cwd})`
13715
+ `Skipping upload: missing required hook fields (session_id=${!!sessionId}, transcript_path=${!!payload.transcript_path}, cwd=${!!cwd})`
13428
13716
  );
13429
- return;
13717
+ return false;
13430
13718
  }
13431
13719
  const match = await findProjectForCwd(cwd);
13432
13720
  if (!match) {
@@ -13434,11 +13722,33 @@ async function runUploadInner(payload) {
13434
13722
  "warn",
13435
13723
  `Skipping session ${sessionId}: no hillclimb config for cwd ${cwd}. Run \`npx hillclimb\` in the repo.`
13436
13724
  );
13437
- return;
13725
+ return false;
13438
13726
  }
13439
13727
  const { repoRoot, config } = match;
13728
+ const sourceTool = await resolveSourceTool(payload, repoRoot);
13729
+ if (!sourceTool) {
13730
+ payload.tool = "unknown";
13731
+ return false;
13732
+ }
13733
+ payload.tool = sourceTool;
13734
+ appendLog(
13735
+ "info",
13736
+ `[${sessionId}] payload parsed (tool=${sourceTool}, cwd=${cwd}, event=${eventKind ?? "?"})`
13737
+ );
13440
13738
  await selfHealHook(repoRoot, sourceTool);
13441
- const transcriptResolved = path14.resolve(transcriptPath);
13739
+ const transcriptPath = await resolveTranscriptPath(
13740
+ payload,
13741
+ sourceTool,
13742
+ sessionId
13743
+ );
13744
+ if (!transcriptPath) {
13745
+ appendLog(
13746
+ "warn",
13747
+ `Skipping upload: missing required hook fields (session_id=true, transcript_path=false, cwd=true)`
13748
+ );
13749
+ return false;
13750
+ }
13751
+ const transcriptResolved = path15.resolve(transcriptPath);
13442
13752
  try {
13443
13753
  const stat = await fs11.promises.stat(transcriptResolved);
13444
13754
  if (!stat.isFile()) {
@@ -13446,23 +13756,23 @@ async function runUploadInner(payload) {
13446
13756
  "warn",
13447
13757
  `Skipping session ${sessionId}: transcript_path is not a file: ${transcriptResolved}`
13448
13758
  );
13449
- return;
13759
+ return false;
13450
13760
  }
13451
13761
  } catch (err) {
13452
13762
  appendLog(
13453
13763
  "warn",
13454
13764
  `Skipping session ${sessionId}: transcript_path not readable (${transcriptResolved}): ${err instanceof Error ? err.message : String(err)}`
13455
13765
  );
13456
- return;
13766
+ return false;
13457
13767
  }
13458
13768
  if (!await hasAssistantMessage(transcriptResolved)) {
13459
13769
  appendLog(
13460
13770
  "info",
13461
13771
  `Skipping session ${sessionId}: transcript contains no assistant messages (nothing to upload).`
13462
13772
  );
13463
- return;
13773
+ return false;
13464
13774
  }
13465
- await uploadSession({
13775
+ return await uploadSession({
13466
13776
  sessionId,
13467
13777
  transcriptPath: transcriptResolved,
13468
13778
  repoRoot,
@@ -13474,14 +13784,16 @@ async function runUploadInner(payload) {
13474
13784
  async function uploadSession(args) {
13475
13785
  const { sessionId, transcriptPath, repoRoot, config, sourceTool, eventKind } = args;
13476
13786
  const isSessionEnd = eventKind === "sessionEnd";
13477
- await withUploadLock(repoRoot, sourceTool, sessionId, async () => {
13787
+ return await withUploadLock(repoRoot, sourceTool, sessionId, async () => {
13478
13788
  const prior = await readUploadState(repoRoot, sourceTool, sessionId);
13479
13789
  let transcriptSize;
13790
+ let transcriptSha256;
13480
13791
  try {
13481
13792
  transcriptSize = (await fs11.promises.stat(transcriptPath)).size;
13793
+ transcriptSha256 = await hashFileSha256(transcriptPath);
13482
13794
  } catch {
13483
13795
  }
13484
- if (prior?.contributionId && transcriptSize !== void 0 && transcriptSize === prior.lastTranscriptSize) {
13796
+ if (prior?.contributionId && transcriptSize !== void 0 && transcriptSha256 !== void 0 && transcriptSize === prior.lastTranscriptSize && transcriptSha256 === prior.lastTranscriptSha256) {
13485
13797
  appendLog(
13486
13798
  "info",
13487
13799
  `[${sessionId}] skipping ${sourceTool} ${isSessionEnd ? "SessionEnd" : "Stop"} upload (transcript unchanged at ${transcriptSize} bytes${isSessionEnd ? "; local state cleared" : ""})`
@@ -13489,7 +13801,7 @@ async function uploadSession(args) {
13489
13801
  if (isSessionEnd) {
13490
13802
  await deleteUploadState(repoRoot, sourceTool, sessionId);
13491
13803
  }
13492
- return;
13804
+ return true;
13493
13805
  }
13494
13806
  const now = /* @__PURE__ */ new Date();
13495
13807
  const sourceFile = {
@@ -13499,13 +13811,13 @@ async function uploadSession(args) {
13499
13811
  };
13500
13812
  const group = {
13501
13813
  repoPath: repoRoot,
13502
- label: path14.basename(repoRoot),
13814
+ label: path15.basename(repoRoot),
13503
13815
  files: [sourceFile],
13504
13816
  sourceNames: [sourceTool],
13505
13817
  lastModified: now
13506
13818
  };
13507
13819
  const envFileNames = await discoverEnvFiles(repoRoot);
13508
- const envFilePaths = envFileNames.map((n) => path14.join(repoRoot, n));
13820
+ const envFilePaths = envFileNames.map((n) => path15.join(repoRoot, n));
13509
13821
  const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
13510
13822
  const mwChain = [];
13511
13823
  if (secretResult.values.size > 0) {
@@ -13519,7 +13831,7 @@ async function uploadSession(args) {
13519
13831
  "error",
13520
13832
  `Session ${sessionId} upload skipped: no saved login for ${config.apiBaseUrl}. Run \`npx hillclimb login\`.`
13521
13833
  );
13522
- return;
13834
+ return false;
13523
13835
  }
13524
13836
  const client = new PlatformClient(
13525
13837
  config.apiBaseUrl,
@@ -13566,7 +13878,8 @@ Uploaded: ${now.toISOString()}`;
13566
13878
  uploadCount: prior?.uploadCount ?? 0,
13567
13879
  firstUploadedAt: prior?.firstUploadedAt ?? now.toISOString(),
13568
13880
  lastUploadedAt: prior?.lastUploadedAt ?? now.toISOString(),
13569
- lastTranscriptSize: prior?.lastTranscriptSize
13881
+ lastTranscriptSize: prior?.lastTranscriptSize,
13882
+ lastTranscriptSha256: prior?.lastTranscriptSha256
13570
13883
  })
13571
13884
  });
13572
13885
  const reuseDesc = prior?.contributionId ? `reusing contribution ${prior.contributionId} (file #${seq})` : prior ? "new contribution (file #1; prior state had no contribution \u2014 earlier create may have failed)" : "new contribution (file #1; first upload for session)";
@@ -13585,13 +13898,13 @@ Uploaded: ${now.toISOString()}`;
13585
13898
  "error",
13586
13899
  `Session ${sessionId} upload failed: authentication expired. Re-run \`npx hillclimb\` in ${repoRoot}.`
13587
13900
  );
13588
- return;
13901
+ return false;
13589
13902
  }
13590
13903
  appendLog(
13591
13904
  "error",
13592
13905
  `Session ${sessionId} upload failed: ${err instanceof Error ? err.message : String(err)}`
13593
13906
  );
13594
- return;
13907
+ return false;
13595
13908
  }
13596
13909
  const next = {
13597
13910
  schemaVersion: CURRENT_SCHEMA_VERSION2,
@@ -13604,7 +13917,8 @@ Uploaded: ${now.toISOString()}`;
13604
13917
  uploadCount: seq,
13605
13918
  firstUploadedAt: prior?.firstUploadedAt ?? now.toISOString(),
13606
13919
  lastUploadedAt: now.toISOString(),
13607
- lastTranscriptSize: transcriptSize
13920
+ lastTranscriptSize: transcriptSize,
13921
+ lastTranscriptSha256: transcriptSha256
13608
13922
  };
13609
13923
  await writeUploadState(next);
13610
13924
  appendLog(
@@ -13618,10 +13932,12 @@ Uploaded: ${now.toISOString()}`;
13618
13932
  `[${sessionId}] session complete \u2014 contribution ${contributionId}, ${seq} file(s); local state cleared`
13619
13933
  );
13620
13934
  }
13935
+ return true;
13621
13936
  });
13622
13937
  }
13623
13938
  var WORKER_ENV_FLAG = "HILLCLIMB_UPLOAD_WORKER";
13624
13939
  var TOOL_ENV_FLAG = "HILLCLIMB_UPLOAD_TOOL";
13940
+ var FLOW_ID_ENV = "HILLCLIMB_UPLOAD_FLOW";
13625
13941
  function parseToolArg(argv) {
13626
13942
  for (let i = 0; i < argv.length; i++) {
13627
13943
  const a = argv[i];
@@ -13635,6 +13951,8 @@ async function runUpload() {
13635
13951
  await runUploadWorker();
13636
13952
  return;
13637
13953
  }
13954
+ const flowId = newFlowId();
13955
+ setLogPrefix(`[${flowId}]`);
13638
13956
  appendLog("info", `upload hook invoked (pid ${process.pid})`);
13639
13957
  let raw;
13640
13958
  try {
@@ -13661,7 +13979,8 @@ async function runUpload() {
13661
13979
  const toolArg = parseToolArg(process.argv.slice(2));
13662
13980
  const workerEnv = {
13663
13981
  ...process.env,
13664
- [WORKER_ENV_FLAG]: "1"
13982
+ [WORKER_ENV_FLAG]: "1",
13983
+ [FLOW_ID_ENV]: flowId
13665
13984
  };
13666
13985
  if (toolArg) workerEnv[TOOL_ENV_FLAG] = toolArg;
13667
13986
  try {
@@ -13688,6 +14007,7 @@ async function runUpload() {
13688
14007
  }
13689
14008
  }
13690
14009
  async function runUploadWorker() {
14010
+ setLogPrefix(`[${process.env[FLOW_ID_ENV] ?? newFlowId()}]`);
13691
14011
  appendLog("info", `upload worker started (pid ${process.pid})`);
13692
14012
  let raw;
13693
14013
  try {
@@ -13716,19 +14036,22 @@ async function runUploadWorker() {
13716
14036
  const toolOverride = process.env[TOOL_ENV_FLAG];
13717
14037
  if (toolOverride) payload.tool = toolOverride;
13718
14038
  appendLog("info", `worker: payload summary: ${summarizePayload(payload)}`);
14039
+ let completed = false;
13719
14040
  try {
13720
- await runUploadInner(payload);
14041
+ completed = await runUploadInner(payload);
13721
14042
  } catch (err) {
13722
14043
  appendLog(
13723
14044
  "error",
13724
14045
  `worker: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
13725
14046
  );
13726
14047
  } finally {
13727
- await recordDebugLogCompletion({
13728
- kind: "agent",
13729
- tool: resolveSourceTool(payload),
13730
- payload
13731
- });
14048
+ if (completed) {
14049
+ await recordDebugLogCompletion({
14050
+ kind: "agent",
14051
+ tool: fallbackSourceTool(payload),
14052
+ payload
14053
+ });
14054
+ }
13732
14055
  try {
13733
14056
  await sweepStaleUploadStates();
13734
14057
  } catch {
@@ -13738,17 +14061,17 @@ async function runUploadWorker() {
13738
14061
 
13739
14062
  // src/git-traces/index.ts
13740
14063
  import { spawn as spawn3 } from "child_process";
13741
- import crypto4 from "crypto";
14064
+ import crypto5 from "crypto";
13742
14065
 
13743
14066
  // src/git-traces/handlers.ts
13744
- import { execFileSync as execFileSync2 } from "child_process";
13745
- import path17 from "path";
14067
+ import { execFileSync as execFileSync3 } from "child_process";
14068
+ import path18 from "path";
13746
14069
 
13747
14070
  // src/git-traces/git-ops.ts
13748
- import { execFileSync } from "child_process";
14071
+ import { execFileSync as execFileSync2 } from "child_process";
13749
14072
  import fs12 from "fs";
13750
14073
  import os7 from "os";
13751
- import path15 from "path";
14074
+ import path16 from "path";
13752
14075
  import { gzipSync } from "zlib";
13753
14076
  var GIT_COMMAND_TIMEOUT_MS = 12e4;
13754
14077
  var EXEC_OPTS = {
@@ -13802,7 +14125,7 @@ function isGitTimeoutError(err) {
13802
14125
  }
13803
14126
  function gitBuffer(repoRoot, args, options = {}) {
13804
14127
  try {
13805
- return execFileSync("git", args, {
14128
+ return execFileSync2("git", args, {
13806
14129
  cwd: repoRoot,
13807
14130
  ...options.env ? { env: options.env } : {},
13808
14131
  ...options.input !== void 0 ? { input: options.input } : {},
@@ -13911,7 +14234,7 @@ function removePathsFromIndex(repoRoot, env, paths) {
13911
14234
  function filterOversizedFilesFromTree(repoRoot, treeSha, options = {}) {
13912
14235
  const oversizedFiles = listOversizedTreeFiles(repoRoot, treeSha, options);
13913
14236
  if (oversizedFiles.length === 0) return treeSha;
13914
- const tmpIndex = path15.join(
14237
+ const tmpIndex = path16.join(
13915
14238
  os7.tmpdir(),
13916
14239
  `hillclimb-filter-${Date.now()}-${process.pid}`
13917
14240
  );
@@ -13943,7 +14266,7 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
13943
14266
  for (const relPath of list.split("\0")) {
13944
14267
  if (!relPath) continue;
13945
14268
  try {
13946
- const stat = fs12.lstatSync(path15.join(repoRoot, relPath));
14269
+ const stat = fs12.lstatSync(path16.join(repoRoot, relPath));
13947
14270
  if (stat.size > MAX_SNAPSHOT_FILE_BYTES) {
13948
14271
  recordOmittedSnapshotFile(omittedFiles, {
13949
14272
  path: relPath,
@@ -13958,7 +14281,7 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
13958
14281
  }
13959
14282
  }
13960
14283
  if (kept.length === 0) return null;
13961
- const tmpIndex = path15.join(
14284
+ const tmpIndex = path16.join(
13962
14285
  os7.tmpdir(),
13963
14286
  `hillclimb-untracked-${Date.now()}-${process.pid}`
13964
14287
  );
@@ -13988,7 +14311,7 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
13988
14311
  const untrackedTree = buildUntrackedTree(repoRoot, options.omittedFiles);
13989
14312
  if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA)
13990
14313
  return filteredTrackedTree;
13991
- const tmpIndex = path15.join(
14314
+ const tmpIndex = path16.join(
13992
14315
  os7.tmpdir(),
13993
14316
  `hillclimb-index-${Date.now()}-${process.pid}`
13994
14317
  );
@@ -14030,7 +14353,7 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
14030
14353
  ]);
14031
14354
  const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
14032
14355
  pinRef(repoRoot, orphanRef, orphanCommit);
14033
- const tmpFile = path15.join(
14356
+ const tmpFile = path16.join(
14034
14357
  os7.tmpdir(),
14035
14358
  // Include the pid (like the other temp files in this module) so concurrent
14036
14359
  // git-traces workers — e.g. two sessions, or a parent + subagent — don't
@@ -14229,9 +14552,9 @@ function parseCommitFiles(repoRoot, sha) {
14229
14552
  oldPath
14230
14553
  });
14231
14554
  } else {
14232
- const path24 = parts[parts.length - 1];
14233
- indexByPath.set(path24, files.length);
14234
- files.push({ path: path24, status, additions: 0, deletions: 0 });
14555
+ const path25 = parts[parts.length - 1];
14556
+ indexByPath.set(path25, files.length);
14557
+ files.push({ path: path25, status, additions: 0, deletions: 0 });
14235
14558
  }
14236
14559
  }
14237
14560
  for (const line of numstat.split("\n")) {
@@ -14297,22 +14620,22 @@ function cleanupSessionRefs(repoRoot, sessionId) {
14297
14620
  }
14298
14621
 
14299
14622
  // src/git-traces/session-state.ts
14300
- import crypto3 from "crypto";
14623
+ import crypto4 from "crypto";
14301
14624
  import fs13 from "fs";
14302
14625
  import os8 from "os";
14303
- import path16 from "path";
14626
+ import path17 from "path";
14304
14627
  var CURRENT_SCHEMA_VERSION3 = 3;
14305
- var DEFAULT_STATE_DIR2 = path16.join(os8.homedir(), ".hillclimb", "git-traces");
14306
- var LOCK_RETRIES3 = 120;
14307
- var LOCK_RETRY_DELAY_MS3 = 500;
14628
+ var DEFAULT_STATE_DIR2 = path17.join(os8.homedir(), ".hillclimb", "git-traces");
14629
+ var LOCK_RETRIES2 = 120;
14630
+ var LOCK_RETRY_DELAY_MS2 = 500;
14308
14631
  function stateDir3() {
14309
14632
  return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR2;
14310
14633
  }
14311
14634
  function stateFileForRepo(repoRoot, tool, sessionId) {
14312
- const hash = crypto3.createHash("sha256").update(
14313
- sessionId ? `${path16.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path16.resolve(repoRoot)}\0${tool}`
14635
+ const hash = crypto4.createHash("sha256").update(
14636
+ sessionId ? `${path17.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path17.resolve(repoRoot)}\0${tool}`
14314
14637
  ).digest("hex").slice(0, 16);
14315
- return path16.join(stateDir3(), `${hash}.json`);
14638
+ return path17.join(stateDir3(), `${hash}.json`);
14316
14639
  }
14317
14640
  function lockFileForRepo(repoRoot, tool) {
14318
14641
  return `${stateFileForRepo(repoRoot, tool)}.lock`;
@@ -14339,14 +14662,14 @@ async function listScopedSessionStates(repoRoot, tool) {
14339
14662
  const states = [];
14340
14663
  for (const entry of entries) {
14341
14664
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
14342
- const file = path16.join(stateDir3(), entry.name);
14665
+ const file = path17.join(stateDir3(), entry.name);
14343
14666
  const state = await readStateFile(file);
14344
14667
  if (!state) continue;
14345
14668
  if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
14346
14669
  continue;
14347
14670
  }
14348
- if (path16.resolve(state.repoRoot) !== path16.resolve(repoRoot)) continue;
14349
- if (path16.resolve(file) !== path16.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
14671
+ if (path17.resolve(state.repoRoot) !== path17.resolve(repoRoot)) continue;
14672
+ if (path17.resolve(file) !== path17.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
14350
14673
  continue;
14351
14674
  }
14352
14675
  let mtimeMs = 0;
@@ -14369,14 +14692,14 @@ async function listSessionStatesForSession(tool, sessionId) {
14369
14692
  const states = [];
14370
14693
  for (const entry of entries) {
14371
14694
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
14372
- const file = path16.join(stateDir3(), entry.name);
14695
+ const file = path17.join(stateDir3(), entry.name);
14373
14696
  const state = await readStateFile(file);
14374
14697
  if (!state) continue;
14375
14698
  if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
14376
14699
  continue;
14377
14700
  }
14378
14701
  if (state.sessionId !== sessionId) continue;
14379
- if (path16.resolve(file) !== path16.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
14702
+ if (path17.resolve(file) !== path17.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
14380
14703
  continue;
14381
14704
  }
14382
14705
  let mtimeMs = 0;
@@ -14432,7 +14755,7 @@ async function deleteSessionState(repoRoot, tool, sessionId) {
14432
14755
  }
14433
14756
  await deleteStateFile(stateFileForRepo(repoRoot, tool));
14434
14757
  }
14435
- async function acquireLock3(repoRoot, tool, retries = LOCK_RETRIES3, delayMs = LOCK_RETRY_DELAY_MS3) {
14758
+ async function acquireLock3(repoRoot, tool, retries = LOCK_RETRIES2, delayMs = LOCK_RETRY_DELAY_MS2) {
14436
14759
  const lockPath = lockFileForRepo(repoRoot, tool);
14437
14760
  await fs13.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
14438
14761
  for (let i = 0; i < retries; i++) {
@@ -14479,18 +14802,18 @@ var TOOL_LABELS = {
14479
14802
  async function loadConfiguredRepos() {
14480
14803
  const file = await loadProjects();
14481
14804
  return Object.entries(file.projects).map(([repoRoot, config]) => ({
14482
- repoRoot: path17.resolve(repoRoot),
14805
+ repoRoot: path18.resolve(repoRoot),
14483
14806
  config
14484
14807
  })).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
14485
14808
  }
14486
14809
  function repoLabel(repoRoot) {
14487
- return path17.basename(repoRoot) || repoRoot;
14810
+ return path18.basename(repoRoot) || repoRoot;
14488
14811
  }
14489
14812
  function resolveCwd2(payload) {
14490
- return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
14813
+ return resolveHookCwd(payload);
14491
14814
  }
14492
14815
  function resolveSessionId2(payload) {
14493
- return payload.session_id ?? payload.conversation_id ?? null;
14816
+ return resolveHookSessionId(payload);
14494
14817
  }
14495
14818
  function epochPrefix(epoch) {
14496
14819
  return `epoch-${String(epoch).padStart(3, "0")}`;
@@ -14506,7 +14829,7 @@ function captureHeadSha(cwd) {
14506
14829
  }
14507
14830
  }
14508
14831
  function execGit(cwd, args) {
14509
- return execFileSync2("git", args, {
14832
+ return execFileSync3("git", args, {
14510
14833
  cwd,
14511
14834
  stdio: ["pipe", "pipe", "pipe"],
14512
14835
  timeout: 3e4,
@@ -14699,27 +15022,34 @@ async function createGitTracesContribution(params) {
14699
15022
  const epochSeconds = formatEpochSeconds3(now);
14700
15023
  const shortId = state.sessionId.slice(0, 12);
14701
15024
  const repoName = repoLabel(repoRoot);
14702
- const contribution = await client.createContribution(config.projectId, {
14703
- contributionTypeSlug: GIT_TRACES_SLUG,
14704
- title: `${toolLabel2} session ${shortId} \u2014 ${repoName} \u2014 ${epochSeconds}`,
14705
- body: `Session ID: ${state.sessionId}
15025
+ let contributionId = state.contributionId;
15026
+ if (!contributionId) {
15027
+ const contribution = await client.createContribution(config.projectId, {
15028
+ contributionTypeSlug: GIT_TRACES_SLUG,
15029
+ title: `${toolLabel2} session ${shortId} \u2014 ${repoName} \u2014 ${epochSeconds}`,
15030
+ body: `Session ID: ${state.sessionId}
14706
15031
  Tool: ${toolLabel2}
14707
15032
  Repo: ${repoRoot}
14708
15033
  Uploaded: ${now.toISOString()}`
14709
- });
15034
+ });
15035
+ contributionId = contribution.id;
15036
+ state.contributionId = contributionId;
15037
+ state.baselineUploaded = false;
15038
+ await writeSessionState(state, tool);
15039
+ }
14710
15040
  const uploaded = await uploadEpochBaselineArtifacts({
14711
15041
  client,
14712
- contributionId: contribution.id,
15042
+ contributionId,
14713
15043
  epoch: 1,
14714
15044
  artifacts
14715
15045
  });
14716
15046
  if (!uploaded) return null;
14717
- await client.submitContribution(contribution.id);
15047
+ await client.submitContribution(contributionId);
14718
15048
  appendLog(
14719
15049
  "info",
14720
- `git-traces: baseline uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contribution.id}, epoch=1, bundleBytes=${artifacts.bundleBuffer.byteLength}, metadataBytes=${artifacts.metadataBuffer.byteLength})`
15050
+ `git-traces: baseline uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId}, epoch=1, bundleBytes=${artifacts.bundleBuffer.byteLength}, metadataBytes=${artifacts.metadataBuffer.byteLength})`
14721
15051
  );
14722
- return contribution.id;
15052
+ return contributionId;
14723
15053
  }
14724
15054
  async function uploadEpochBaseline(params) {
14725
15055
  const artifacts = buildEpochBaselineArtifacts(params);
@@ -14760,6 +15090,7 @@ async function initializeSession(repoRoot, tool, sessionId) {
14760
15090
  schemaVersion: CURRENT_SCHEMA_VERSION3,
14761
15091
  sessionId,
14762
15092
  contributionId: null,
15093
+ baselineUploaded: false,
14763
15094
  baselineSha: frozen.baselineSha,
14764
15095
  baselineTreeSha: frozen.baselineTreeSha,
14765
15096
  baselineMetadata: frozen.baselineMetadata,
@@ -14920,6 +15251,7 @@ async function registerInitialContribution(params) {
14920
15251
  });
14921
15252
  if (!contributionId) return false;
14922
15253
  state.contributionId = contributionId;
15254
+ state.baselineUploaded = true;
14923
15255
  state.baselineTreeSha = artifacts.baselineTreeSha;
14924
15256
  state.lastSnapshotTreeSha = artifacts.baselineTreeSha;
14925
15257
  await writeSessionState(state, tool);
@@ -14961,7 +15293,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
14961
15293
  if (currentHeadSha !== state.headSha) {
14962
15294
  const client2 = await loadRepoClient(repo);
14963
15295
  if (!client2) return "skipped";
14964
- if (state.contributionId === null) {
15296
+ if (state.contributionId === null || !state.baselineUploaded) {
14965
15297
  const artifacts2 = buildInitialBaselineArtifactsForState(
14966
15298
  repoRoot,
14967
15299
  tool,
@@ -15044,7 +15376,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
15044
15376
  const nextTurnCount = state.turnCount + 1;
15045
15377
  const turnLabel = turnSuffix(nextTurnCount);
15046
15378
  const filename = `${prefix}-${turnLabel}-${recordedAt}.patch.gz`;
15047
- if (state.contributionId === null && !canUploadFile(filename, patchBuffer)) {
15379
+ if ((state.contributionId === null || !state.baselineUploaded) && !canUploadFile(filename, patchBuffer)) {
15048
15380
  appendLog(
15049
15381
  "warn",
15050
15382
  `git-traces: first changed turn skipped before contribution creation (repo=${repoRoot}, project=${config.projectId}, reason=patch-too-large)`
@@ -15053,7 +15385,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
15053
15385
  }
15054
15386
  const client = await loadRepoClient(repo);
15055
15387
  if (!client) return "skipped";
15056
- if (state.contributionId === null) {
15388
+ if (state.contributionId === null || !state.baselineUploaded) {
15057
15389
  const artifacts = buildInitialBaselineArtifactsForState(
15058
15390
  repoRoot,
15059
15391
  tool,
@@ -15118,6 +15450,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
15118
15450
  {
15119
15451
  ...state,
15120
15452
  contributionId: null,
15453
+ baselineUploaded: false,
15121
15454
  lastSnapshotSha: state.baselineSha,
15122
15455
  lastSnapshotTreeSha: state.baselineTreeSha,
15123
15456
  turnCount: 0
@@ -15156,7 +15489,7 @@ async function handleStop(payload, tool) {
15156
15489
  if (sessionId) {
15157
15490
  const storedStates = await listSessionStatesForSession(tool, sessionId);
15158
15491
  for (const { state } of storedStates) {
15159
- const repo = repoByRoot.get(path17.resolve(state.repoRoot));
15492
+ const repo = repoByRoot.get(path18.resolve(state.repoRoot));
15160
15493
  if (!repo) {
15161
15494
  missingConfig++;
15162
15495
  appendLog(
@@ -15218,21 +15551,62 @@ async function handleSessionEnd(payload, tool) {
15218
15551
  const sessionId = resolveSessionId2(payload);
15219
15552
  const project = cwd ? await findProjectForCwd(cwd) : null;
15220
15553
  const triggerRepo = project?.repoRoot ?? cwd ?? "<none>";
15554
+ const recordedAt = Date.now();
15221
15555
  const repoRoots = [];
15222
15556
  if (sessionId) {
15223
15557
  const states = await listSessionStatesForSession(tool, sessionId);
15224
15558
  for (const { state } of states) {
15225
- repoRoots.push(path17.resolve(state.repoRoot));
15559
+ repoRoots.push(path18.resolve(state.repoRoot));
15226
15560
  }
15227
15561
  }
15228
15562
  if (repoRoots.length === 0 && cwd) {
15229
15563
  repoRoots.push(project?.repoRoot ?? cwd);
15230
15564
  }
15231
15565
  if (repoRoots.length === 0) return;
15566
+ const repos = await loadConfiguredRepos();
15567
+ const repoByRoot = new Map(repos.map((repo) => [repo.repoRoot, repo]));
15232
15568
  let cleaned = 0;
15233
15569
  let noState = 0;
15570
+ let uploaded = 0;
15571
+ let unchanged = 0;
15572
+ let skipped = 0;
15234
15573
  let failed = 0;
15235
15574
  for (const repoRoot of repoRoots) {
15575
+ const repo = repoByRoot.get(path18.resolve(repoRoot)) ?? (project && path18.resolve(project.repoRoot) === path18.resolve(repoRoot) ? { repoRoot: project.repoRoot, config: project.config } : null);
15576
+ if (!repo) {
15577
+ skipped++;
15578
+ appendLog(
15579
+ "warn",
15580
+ `git-traces: preserving SessionEnd state for repo ${repoRoot} (reason=missing-config)`
15581
+ );
15582
+ continue;
15583
+ }
15584
+ const finalOutcome = await processStopRepo(
15585
+ repo,
15586
+ tool,
15587
+ sessionId,
15588
+ recordedAt
15589
+ );
15590
+ if (finalOutcome === "uploaded") uploaded++;
15591
+ else if (finalOutcome === "unchanged") unchanged++;
15592
+ else if (finalOutcome === "skipped") {
15593
+ skipped++;
15594
+ appendLog(
15595
+ "warn",
15596
+ `git-traces: preserving SessionEnd state for repo ${repoRoot} after skipped final upload`
15597
+ );
15598
+ continue;
15599
+ } else if (finalOutcome === "failed") {
15600
+ failed++;
15601
+ appendLog(
15602
+ "warn",
15603
+ `git-traces: preserving SessionEnd state for repo ${repoRoot} after failed final upload`
15604
+ );
15605
+ continue;
15606
+ } else {
15607
+ noState++;
15608
+ continue;
15609
+ }
15236
15610
  const outcome = await cleanupSessionStateForRepo(repoRoot, tool, sessionId);
15237
15611
  if (outcome === "cleaned") cleaned++;
15238
15612
  else if (outcome === "no-state") noState++;
@@ -15240,16 +15614,16 @@ async function handleSessionEnd(payload, tool) {
15240
15614
  }
15241
15615
  appendLog(
15242
15616
  "info",
15243
- `git-traces: SessionEnd summary (session=${sessionId ?? "<none>"}, tool=${tool}, triggerRepo=${triggerRepo}, states=${repoRoots.length}, cleaned=${cleaned}, noState=${noState}, failed=${failed})`
15617
+ `git-traces: SessionEnd summary (session=${sessionId ?? "<none>"}, tool=${tool}, triggerRepo=${triggerRepo}, states=${repoRoots.length}, uploaded=${uploaded}, unchanged=${unchanged}, skipped=${skipped}, cleaned=${cleaned}, noState=${noState}, failed=${failed})`
15244
15618
  );
15245
15619
  }
15246
15620
 
15247
15621
  // src/git-traces/index.ts
15248
15622
  var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
15249
15623
  var TOOL_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_TOOL";
15250
- var FLOW_ID_ENV = "HILLCLIMB_GIT_TRACES_FLOW";
15251
- function newFlowId() {
15252
- return crypto4.randomBytes(3).toString("hex");
15624
+ var FLOW_ID_ENV2 = "HILLCLIMB_GIT_TRACES_FLOW";
15625
+ function newFlowId2() {
15626
+ return crypto5.randomBytes(3).toString("hex");
15253
15627
  }
15254
15628
  var KNOWN_TOOLS = /* @__PURE__ */ new Set([
15255
15629
  "claude",
@@ -15388,7 +15762,7 @@ async function runGitTraces() {
15388
15762
  await runGitTracesWorker();
15389
15763
  return;
15390
15764
  }
15391
- const flowId = newFlowId();
15765
+ const flowId = newFlowId2();
15392
15766
  setLogPrefix(`[${flowId}]`);
15393
15767
  const toolArg = parseToolArg2(process.argv.slice(2));
15394
15768
  appendLog(
@@ -15431,7 +15805,7 @@ async function runGitTraces() {
15431
15805
  ...process.env,
15432
15806
  [WORKER_ENV_FLAG2]: "1",
15433
15807
  [TOOL_ENV_FLAG2]: tool,
15434
- [FLOW_ID_ENV]: flowId
15808
+ [FLOW_ID_ENV2]: flowId
15435
15809
  }
15436
15810
  }
15437
15811
  );
@@ -15453,7 +15827,7 @@ async function runGitTraces() {
15453
15827
  }
15454
15828
  }
15455
15829
  async function runGitTracesWorker() {
15456
- setLogPrefix(`[${process.env[FLOW_ID_ENV] ?? newFlowId()}]`);
15830
+ setLogPrefix(`[${process.env[FLOW_ID_ENV2] ?? newFlowId2()}]`);
15457
15831
  const tool = process.env[TOOL_ENV_FLAG2] ?? parseToolArg2(process.argv.slice(2)) ?? null;
15458
15832
  appendLog(
15459
15833
  "info",
@@ -15536,14 +15910,14 @@ ${stack}` : ""}`
15536
15910
 
15537
15911
  // src/outputs/zip.ts
15538
15912
  import fs15 from "fs";
15539
- import path19 from "path";
15913
+ import path20 from "path";
15540
15914
  import archiver2 from "archiver";
15541
15915
 
15542
15916
  // src/outputs/downloads.ts
15543
15917
  import { execSync as execSync2 } from "child_process";
15544
15918
  import fs14 from "fs";
15545
15919
  import os9 from "os";
15546
- import path18 from "path";
15920
+ import path19 from "path";
15547
15921
  function getDownloadsFolder() {
15548
15922
  const home = os9.homedir();
15549
15923
  if (process.platform === "linux") {
@@ -15556,7 +15930,7 @@ function getDownloadsFolder() {
15556
15930
  } catch {
15557
15931
  }
15558
15932
  }
15559
- const downloads = path18.join(home, "Downloads");
15933
+ const downloads = path19.join(home, "Downloads");
15560
15934
  if (fs14.existsSync(downloads)) return downloads;
15561
15935
  return home;
15562
15936
  }
@@ -15566,11 +15940,11 @@ function sanitizeFilename(name) {
15566
15940
  return name.replace(/[^a-zA-Z0-9._-]/g, "_");
15567
15941
  }
15568
15942
  function getUniqueFilename(dir, base, ext) {
15569
- let candidate = path19.join(dir, `${base}${ext}`);
15943
+ let candidate = path20.join(dir, `${base}${ext}`);
15570
15944
  if (!fs15.existsSync(candidate)) return candidate;
15571
15945
  let i = 1;
15572
15946
  while (fs15.existsSync(candidate)) {
15573
- candidate = path19.join(dir, `${base}-${i}${ext}`);
15947
+ candidate = path20.join(dir, `${base}-${i}${ext}`);
15574
15948
  i++;
15575
15949
  }
15576
15950
  return candidate;
@@ -15580,7 +15954,7 @@ var ZipOutput = class {
15580
15954
  label = "Save as .zip to Downloads";
15581
15955
  async emit(group, options) {
15582
15956
  const downloadsDir = getDownloadsFolder();
15583
- const repoName = sanitizeFilename(path19.basename(group.repoPath));
15957
+ const repoName = sanitizeFilename(path20.basename(group.repoPath));
15584
15958
  const timeRange = options.timeRange;
15585
15959
  const rangePart = timeRange?.label ?? "all";
15586
15960
  const epochSeconds = Math.floor(Date.now() / 1e3);
@@ -15782,11 +16156,11 @@ async function confirmExport(group, output) {
15782
16156
  // src/sources/claude.ts
15783
16157
  import fs16 from "fs";
15784
16158
  import os10 from "os";
15785
- import path20 from "path";
15786
- import readline from "readline";
16159
+ import path21 from "path";
16160
+ import readline2 from "readline";
15787
16161
  var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
15788
16162
  async function resolveRepoPath(projectDir) {
15789
- const indexPath = path20.join(projectDir, "sessions-index.json");
16163
+ const indexPath = path21.join(projectDir, "sessions-index.json");
15790
16164
  try {
15791
16165
  const raw = await fs16.promises.readFile(indexPath, "utf-8");
15792
16166
  const data = JSON.parse(raw);
@@ -15801,7 +16175,7 @@ async function resolveRepoPath(projectDir) {
15801
16175
  });
15802
16176
  for (const entry of entries) {
15803
16177
  if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
15804
- const cwd = await extractCwdFromJsonl(path20.join(projectDir, entry.name));
16178
+ const cwd = await extractCwdFromJsonl(path21.join(projectDir, entry.name));
15805
16179
  if (cwd) {
15806
16180
  cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
15807
16181
  }
@@ -15821,7 +16195,7 @@ async function resolveRepoPath(projectDir) {
15821
16195
  }
15822
16196
  async function extractCwdFromJsonl(filePath) {
15823
16197
  const stream = fs16.createReadStream(filePath, { encoding: "utf-8" });
15824
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
16198
+ const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
15825
16199
  try {
15826
16200
  for await (const line of rl) {
15827
16201
  if (!line.trim()) continue;
@@ -15847,7 +16221,7 @@ async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
15847
16221
  return;
15848
16222
  }
15849
16223
  for (const entry of entries) {
15850
- const fullPath = path20.join(dir, entry.name);
16224
+ const fullPath = path21.join(dir, entry.name);
15851
16225
  if (entry.isDirectory()) {
15852
16226
  if (SKIP_DIRS.has(entry.name)) continue;
15853
16227
  await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
@@ -15869,7 +16243,7 @@ function fallbackDecode(encodedName) {
15869
16243
  var ClaudeSource = class {
15870
16244
  name = "claude";
15871
16245
  async scan() {
15872
- const baseDir = path20.join(os10.homedir(), ".claude", "projects");
16246
+ const baseDir = path21.join(os10.homedir(), ".claude", "projects");
15873
16247
  try {
15874
16248
  await fs16.promises.access(baseDir);
15875
16249
  } catch {
@@ -15881,7 +16255,7 @@ var ClaudeSource = class {
15881
16255
  const dirEntries = projectDirs.filter((d) => d.isDirectory());
15882
16256
  const resultArrays = await Promise.all(
15883
16257
  dirEntries.map(async (dir) => {
15884
- const projectPath = path20.join(baseDir, dir.name);
16258
+ const projectPath = path21.join(baseDir, dir.name);
15885
16259
  const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
15886
16260
  const files = [];
15887
16261
  await collectFiles(
@@ -15901,11 +16275,11 @@ var ClaudeSource = class {
15901
16275
  // src/sources/codex.ts
15902
16276
  import fs17 from "fs";
15903
16277
  import os11 from "os";
15904
- import path21 from "path";
15905
- import readline2 from "readline";
16278
+ import path22 from "path";
16279
+ import readline3 from "readline";
15906
16280
  async function parseSessionMeta(filePath) {
15907
16281
  const stream = fs17.createReadStream(filePath, { encoding: "utf-8" });
15908
- const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
16282
+ const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
15909
16283
  try {
15910
16284
  for await (const line of rl) {
15911
16285
  if (!line.trim()) continue;
@@ -15934,7 +16308,7 @@ async function findJsonlFiles(dir) {
15934
16308
  return;
15935
16309
  }
15936
16310
  for (const entry of entries) {
15937
- const full = path21.join(d, entry.name);
16311
+ const full = path22.join(d, entry.name);
15938
16312
  if (entry.isDirectory()) {
15939
16313
  await walk(full);
15940
16314
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -15953,7 +16327,7 @@ async function loadHistory(historyPath) {
15953
16327
  return map;
15954
16328
  }
15955
16329
  const stream = fs17.createReadStream(historyPath, { encoding: "utf-8" });
15956
- const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
16330
+ const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
15957
16331
  try {
15958
16332
  for await (const line of rl) {
15959
16333
  if (!line.trim()) continue;
@@ -15979,14 +16353,14 @@ async function loadHistory(historyPath) {
15979
16353
  var CodexSource = class {
15980
16354
  name = "codex";
15981
16355
  async scan() {
15982
- const codexDir = path21.join(os11.homedir(), ".codex");
15983
- const sessionsDir = path21.join(codexDir, "sessions");
16356
+ const codexDir = path22.join(os11.homedir(), ".codex");
16357
+ const sessionsDir = path22.join(codexDir, "sessions");
15984
16358
  try {
15985
16359
  await fs17.promises.access(sessionsDir);
15986
16360
  } catch {
15987
16361
  return [];
15988
16362
  }
15989
- const historyPath = path21.join(codexDir, "history.jsonl");
16363
+ const historyPath = path22.join(codexDir, "history.jsonl");
15990
16364
  const [jsonlFiles, historyMap] = await Promise.all([
15991
16365
  findJsonlFiles(sessionsDir),
15992
16366
  loadHistory(historyPath)
@@ -16009,8 +16383,8 @@ var CodexSource = class {
16009
16383
  });
16010
16384
  const historyLines = historyMap.get(meta.sessionId);
16011
16385
  if (historyLines) {
16012
- const sessionDir = path21.relative(sessionsDir, path21.dirname(filePath));
16013
- const historyAbsPath = path21.join(
16386
+ const sessionDir = path22.relative(sessionsDir, path22.dirname(filePath));
16387
+ const historyAbsPath = path22.join(
16014
16388
  sessionsDir,
16015
16389
  sessionDir,
16016
16390
  `history-${meta.sessionId}.jsonl`
@@ -16032,16 +16406,16 @@ var CodexSource = class {
16032
16406
  // src/sources/copilotChat.ts
16033
16407
  import fs18 from "fs";
16034
16408
  import os12 from "os";
16035
- import path22 from "path";
16409
+ import path23 from "path";
16036
16410
  import { fileURLToPath } from "url";
16037
16411
  function vsCodeUserDirs() {
16038
16412
  const home = os12.homedir();
16039
16413
  const dirs = [
16040
- path22.join(home, "Library", "Application Support", "Code", "User"),
16041
- path22.join(home, ".config", "Code", "User")
16414
+ path23.join(home, "Library", "Application Support", "Code", "User"),
16415
+ path23.join(home, ".config", "Code", "User")
16042
16416
  ];
16043
16417
  if (process.env.APPDATA) {
16044
- dirs.push(path22.join(process.env.APPDATA, "Code", "User"));
16418
+ dirs.push(path23.join(process.env.APPDATA, "Code", "User"));
16045
16419
  }
16046
16420
  return dirs;
16047
16421
  }
@@ -16078,7 +16452,7 @@ var CopilotChatSource = class {
16078
16452
  async scan() {
16079
16453
  const results = [];
16080
16454
  for (const userDir of vsCodeUserDirs()) {
16081
- const workspaceStorage = path22.join(userDir, "workspaceStorage");
16455
+ const workspaceStorage = path23.join(userDir, "workspaceStorage");
16082
16456
  let hashDirs;
16083
16457
  try {
16084
16458
  hashDirs = await fs18.promises.readdir(workspaceStorage, {
@@ -16089,8 +16463,8 @@ var CopilotChatSource = class {
16089
16463
  }
16090
16464
  for (const hash of hashDirs) {
16091
16465
  if (!hash.isDirectory()) continue;
16092
- const wsRoot = path22.join(workspaceStorage, hash.name);
16093
- const transcriptsDir = path22.join(
16466
+ const wsRoot = path23.join(workspaceStorage, hash.name);
16467
+ const transcriptsDir = path23.join(
16094
16468
  wsRoot,
16095
16469
  "GitHub.copilot-chat",
16096
16470
  "transcripts"
@@ -16104,7 +16478,7 @@ var CopilotChatSource = class {
16104
16478
  continue;
16105
16479
  }
16106
16480
  const repoPath = await readWorkspaceFolder(
16107
- path22.join(wsRoot, "workspace.json")
16481
+ path23.join(wsRoot, "workspace.json")
16108
16482
  );
16109
16483
  if (!repoPath) continue;
16110
16484
  for (const entry of transcriptEntries) {
@@ -16112,7 +16486,7 @@ var CopilotChatSource = class {
16112
16486
  const sessionId = entry.name.slice(0, -".jsonl".length);
16113
16487
  results.push({
16114
16488
  sourceName: this.name,
16115
- absolutePath: path22.join(transcriptsDir, entry.name),
16489
+ absolutePath: path23.join(transcriptsDir, entry.name),
16116
16490
  repoPath,
16117
16491
  metadata: { sessionId }
16118
16492
  });
@@ -16179,10 +16553,10 @@ async function runInteractive() {
16179
16553
  s.start(`Scanning ${source.name} logs...`);
16180
16554
  const allFiles = await source.scan();
16181
16555
  const allGroups = await mergeByRepo(allFiles);
16182
- const repoRoot = path23.resolve(repo.root);
16556
+ const repoRoot = path24.resolve(repo.root);
16183
16557
  const matching = allGroups.filter((g) => {
16184
- const resolved = path23.resolve(g.repoPath);
16185
- return resolved === repoRoot || resolved.startsWith(repoRoot + path23.sep);
16558
+ const resolved = path24.resolve(g.repoPath);
16559
+ return resolved === repoRoot || resolved.startsWith(repoRoot + path24.sep);
16186
16560
  });
16187
16561
  if (matching.length === 0) {
16188
16562
  s.stop(`No ${source.name} logs found for ${repo.name}.`);
@@ -16213,7 +16587,7 @@ async function runInteractive() {
16213
16587
  }
16214
16588
  }
16215
16589
  const envFileNames = await discoverEnvFiles(repoRoot);
16216
- const envFilePaths = envFileNames.map((n) => path23.join(repoRoot, n));
16590
+ const envFilePaths = envFileNames.map((n) => path24.join(repoRoot, n));
16217
16591
  const additionalFiles = await promptSecretFiles(envFileNames);
16218
16592
  const secretResult = await collectSecrets(
16219
16593
  repoRoot,