hillclimb 0.5.0 → 0.5.1

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 +504 -229
  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,15 @@ 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";
2185
2322
 
2186
2323
  // src/debug-logs.ts
2187
2324
  import crypto from "crypto";
2188
2325
  import fs9 from "fs";
2189
- import path11 from "path";
2326
+ import path12 from "path";
2190
2327
 
2191
2328
  // src/hook-events.ts
2192
2329
  function classifyHookEvent(event) {
@@ -2205,6 +2342,41 @@ function classifyHookEvent(event) {
2205
2342
  }
2206
2343
  }
2207
2344
 
2345
+ // src/hook-payload.ts
2346
+ function resolveHookCwd(payload) {
2347
+ return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
2348
+ }
2349
+ function resolveHookSessionId(payload) {
2350
+ return payload.session_id ?? payload.conversation_id ?? null;
2351
+ }
2352
+ function inferSourceToolFromPayload(payload) {
2353
+ if (payload.tool) return payload.tool;
2354
+ if (payload.cursor_version) return "cursor";
2355
+ const transcriptPath = payload.transcript_path ?? "";
2356
+ if (transcriptPath.includes(".claude/projects/")) return "claude";
2357
+ if (transcriptPath.includes(".cursor/projects/")) return "cursor";
2358
+ if (transcriptPath.includes("GitHub.copilot-chat")) return "copilot-chat";
2359
+ if (transcriptPath.includes(".hillclimb/opencode-transcripts/")) {
2360
+ return "opencode";
2361
+ }
2362
+ if (transcriptPath.includes(".codex/")) return "codex";
2363
+ switch (payload.hook_event_name) {
2364
+ case "session.idle":
2365
+ case "session.deleted":
2366
+ case "server.instance.disposed":
2367
+ return "opencode";
2368
+ default:
2369
+ return null;
2370
+ }
2371
+ }
2372
+ function fallbackSourceTool(payload) {
2373
+ const inferred = inferSourceToolFromPayload(payload);
2374
+ if (inferred) return inferred;
2375
+ const eventKind = classifyHookEvent(payload.hook_event_name);
2376
+ if (eventKind === "stop") return "codex";
2377
+ return "claude";
2378
+ }
2379
+
2208
2380
  // src/middleware/pattern-redact.ts
2209
2381
  import os3 from "os";
2210
2382
  import { Worker } from "worker_threads";
@@ -10692,7 +10864,7 @@ var middleware = [];
10692
10864
 
10693
10865
  // src/middleware/secrets.ts
10694
10866
  import fs7 from "fs";
10695
- import path8 from "path";
10867
+ import path9 from "path";
10696
10868
  var KNOWN_NON_SECRETS = /* @__PURE__ */ new Set([
10697
10869
  "true",
10698
10870
  "false",
@@ -10847,7 +11019,7 @@ async function discoverEnvFiles(repoRoot) {
10847
11019
  const envFiles = [];
10848
11020
  for (const name of entries) {
10849
11021
  if (!name.startsWith(".env")) continue;
10850
- const filePath = path8.join(repoRoot, name);
11022
+ const filePath = path9.join(repoRoot, name);
10851
11023
  try {
10852
11024
  const stat = await fs7.promises.stat(filePath);
10853
11025
  if (stat.isFile()) envFiles.push(name);
@@ -10872,7 +11044,7 @@ async function collectSecrets(repoRoot, envFiles, additionalFiles) {
10872
11044
  }
10873
11045
  }
10874
11046
  for (const filePath of additionalFiles) {
10875
- const resolved = path8.resolve(repoRoot, filePath);
11047
+ const resolved = path9.resolve(repoRoot, filePath);
10876
11048
  sourceFiles.push(resolved);
10877
11049
  for (const value of await parseEnvFile(resolved)) {
10878
11050
  if (isUsableValue(value)) {
@@ -10902,16 +11074,16 @@ import archiver from "archiver";
10902
11074
 
10903
11075
  // src/outputs/archive.ts
10904
11076
  import os4 from "os";
10905
- import path9 from "path";
11077
+ import path10 from "path";
10906
11078
  function getSourceBaseDir(sourceName) {
10907
11079
  const home = os4.homedir();
10908
11080
  switch (sourceName) {
10909
11081
  case "claude":
10910
- return path9.join(home, ".claude", "projects");
11082
+ return path10.join(home, ".claude", "projects");
10911
11083
  case "codex":
10912
- return path9.join(home, ".codex", "sessions");
11084
+ return path10.join(home, ".codex", "sessions");
10913
11085
  case "debug-logs":
10914
- return path9.join(configDir(), "logs");
11086
+ return path10.join(configDir(), "logs");
10915
11087
  default:
10916
11088
  return home;
10917
11089
  }
@@ -10920,8 +11092,8 @@ function addGroupToArchive(archive, group, selectedSources) {
10920
11092
  for (const file of group.files) {
10921
11093
  if (!selectedSources.has(file.sourceName)) continue;
10922
11094
  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);
11095
+ const relativePath = file.absolutePath.startsWith(baseDir) ? path10.relative(baseDir, file.absolutePath) : path10.basename(file.absolutePath);
11096
+ const archivePath = path10.join(file.sourceName, relativePath);
10925
11097
  if (file.content) {
10926
11098
  archive.append(file.content, { name: archivePath });
10927
11099
  } else {
@@ -11005,20 +11177,20 @@ var PlatformUploadOutput = class {
11005
11177
 
11006
11178
  // src/pipeline.ts
11007
11179
  import fs8 from "fs";
11008
- import path10 from "path";
11180
+ import path11 from "path";
11009
11181
  function canonicalizePath(p7) {
11010
- let resolved = path10.resolve(p7);
11011
- if (resolved.endsWith(path10.sep) && resolved !== path10.sep) {
11182
+ let resolved = path11.resolve(p7);
11183
+ if (resolved.endsWith(path11.sep) && resolved !== path11.sep) {
11012
11184
  resolved = resolved.slice(0, -1);
11013
11185
  }
11014
11186
  return resolved;
11015
11187
  }
11016
11188
  function computeLabel(repoPath, allPaths) {
11017
- const segments = repoPath.split(path10.sep).filter(Boolean);
11189
+ const segments = repoPath.split(path11.sep).filter(Boolean);
11018
11190
  for (let depth = 1; depth <= segments.length; depth++) {
11019
11191
  const label = segments.slice(-depth).join("/");
11020
11192
  const matches = allPaths.filter((p7) => {
11021
- const s = p7.split(path10.sep).filter(Boolean);
11193
+ const s = p7.split(path11.sep).filter(Boolean);
11022
11194
  return s.slice(-depth).join("/") === label;
11023
11195
  });
11024
11196
  if (matches.length === 1) return label;
@@ -11096,7 +11268,7 @@ var DEFAULT_WAIT_MS = 6e4;
11096
11268
  var LOCK_RETRIES = 100;
11097
11269
  var LOCK_RETRY_DELAY_MS = 100;
11098
11270
  function stateDir() {
11099
- return process.env.HILLCLIMB_DEBUG_LOG_STATE_DIR ?? path11.join(configDir(), "debug-log-uploads");
11271
+ return process.env.HILLCLIMB_DEBUG_LOG_STATE_DIR ?? path12.join(configDir(), "debug-log-uploads");
11100
11272
  }
11101
11273
  function waitMs() {
11102
11274
  const raw = process.env.HILLCLIMB_DEBUG_LOG_WAIT_MS;
@@ -11105,7 +11277,7 @@ function waitMs() {
11105
11277
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_WAIT_MS;
11106
11278
  }
11107
11279
  function stateFile(eventId) {
11108
- return path11.join(stateDir(), `${eventId}.json`);
11280
+ return path12.join(stateDir(), `${eventId}.json`);
11109
11281
  }
11110
11282
  function lockFile(eventId) {
11111
11283
  return `${stateFile(eventId)}.lock`;
@@ -11130,10 +11302,10 @@ function toolLabel(tool) {
11130
11302
  return labels[tool] ?? tool;
11131
11303
  }
11132
11304
  function resolveCwd(payload) {
11133
- return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
11305
+ return resolveHookCwd(payload);
11134
11306
  }
11135
11307
  function resolveSessionId(payload) {
11136
- return payload.session_id ?? payload.conversation_id ?? null;
11308
+ return resolveHookSessionId(payload);
11137
11309
  }
11138
11310
  function stringOrNull(value) {
11139
11311
  return typeof value === "string" && value.length > 0 ? value : null;
@@ -11152,7 +11324,7 @@ function expectedKinds(tool, eventKind, payload) {
11152
11324
  async function transcriptFingerprint(payload) {
11153
11325
  const transcriptPath = stringOrNull(payload.transcript_path);
11154
11326
  if (!transcriptPath) return {};
11155
- const resolved = path11.resolve(transcriptPath);
11327
+ const resolved = path12.resolve(transcriptPath);
11156
11328
  try {
11157
11329
  const stat = await fs9.promises.stat(resolved);
11158
11330
  return {
@@ -11191,13 +11363,10 @@ async function eventContext(tool, payload) {
11191
11363
  conversationId: stringOrNull(payload.conversation_id),
11192
11364
  turnId,
11193
11365
  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) : {}
11366
+ // Only stop events without stable turn IDs need the transcript to
11367
+ // disambiguate. When a turn_id exists, including mutable transcript
11368
+ // mtime/size can split agent+git completions for the same logical event.
11369
+ ...eventKind === "stop" && !turnId ? await transcriptFingerprint(payload) : {}
11201
11370
  };
11202
11371
  const eventId = crypto.createHash("sha256").update(JSON.stringify(fingerprint)).digest("hex").slice(0, 32);
11203
11372
  return {
@@ -11268,7 +11437,7 @@ function initialState(ctx, now) {
11268
11437
  eventKind: ctx.eventKind,
11269
11438
  hookEventName: ctx.hookEventName,
11270
11439
  sessionId: ctx.sessionId,
11271
- logDate: path11.basename(todayLogPath(), ".log"),
11440
+ logDate: path12.basename(todayLogPath(), ".log"),
11272
11441
  firstSeenAt: now.toISOString()
11273
11442
  };
11274
11443
  }
@@ -11330,7 +11499,7 @@ async function waitForExpectedKinds(ctx, state) {
11330
11499
  }
11331
11500
  async function buildMiddleware(repoRoot) {
11332
11501
  const envFileNames = await discoverEnvFiles(repoRoot);
11333
- const envFilePaths = envFileNames.map((n) => path11.join(repoRoot, n));
11502
+ const envFilePaths = envFileNames.map((n) => path12.join(repoRoot, n));
11334
11503
  const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
11335
11504
  const middleware2 = [];
11336
11505
  if (secretResult.values.size > 0) {
@@ -11376,7 +11545,7 @@ async function uploadDebugLog(ctx, state) {
11376
11545
  };
11377
11546
  const group = {
11378
11547
  repoPath: ctx.repoRoot,
11379
- label: path11.basename(ctx.repoRoot),
11548
+ label: path12.basename(ctx.repoRoot),
11380
11549
  files: [sourceFile],
11381
11550
  sourceNames: [DEBUG_LOGS_SLUG],
11382
11551
  lastModified: now
@@ -11398,7 +11567,7 @@ async function uploadDebugLog(ctx, state) {
11398
11567
  `Tool: ${label}`,
11399
11568
  `Event: ${ctx.hookEventName ?? ctx.eventKind}`,
11400
11569
  `Repo: ${ctx.repoRoot}`,
11401
- `Log: ${path11.basename(logPath)}`,
11570
+ `Log: ${path12.basename(logPath)}`,
11402
11571
  `Agent done: ${state.agentDoneAt ?? "<not observed>"}`,
11403
11572
  `Git done: ${state.gitDoneAt ?? "<not observed>"}`,
11404
11573
  `Uploaded: ${now.toISOString()}`
@@ -11415,7 +11584,7 @@ async function uploadDebugLog(ctx, state) {
11415
11584
  );
11416
11585
  appendLog(
11417
11586
  "info",
11418
- `debug-logs: uploaded ${path11.basename(logPath)} to project ${ctx.config.projectSlug} (${ctx.config.projectId}) as contribution ${contributionId}`
11587
+ `debug-logs: uploaded ${path12.basename(logPath)} to project ${ctx.config.projectSlug} (${ctx.config.projectId}) as contribution ${contributionId}`
11419
11588
  );
11420
11589
  return contributionId;
11421
11590
  } catch (err) {
@@ -11468,7 +11637,7 @@ async function recordDebugLogCompletion(args) {
11468
11637
  }
11469
11638
 
11470
11639
  // src/normalizer/index.ts
11471
- import path12 from "path";
11640
+ import path13 from "path";
11472
11641
 
11473
11642
  // src/normalizer/claude.ts
11474
11643
  function stringify(value) {
@@ -13166,14 +13335,20 @@ var NormalizeMiddleware = class {
13166
13335
  continue;
13167
13336
  const content = file.content ? file.content.toString("utf-8") : null;
13168
13337
  if (!content) continue;
13169
- const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 : path12.basename(file.absolutePath, ".jsonl"));
13338
+ const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 : path13.basename(file.absolutePath, ".jsonl"));
13170
13339
  try {
13171
13340
  const trajectory = normalizeContent(
13172
13341
  file.sourceName,
13173
13342
  content,
13174
13343
  sessionId
13175
13344
  );
13176
- if (!trajectory) continue;
13345
+ if (!trajectory) {
13346
+ appendLog(
13347
+ "warn",
13348
+ `normalize: produced no ATIF for source=${file.sourceName} session=${sessionId ?? "<unknown>"} file=${file.absolutePath}`
13349
+ );
13350
+ continue;
13351
+ }
13177
13352
  const json = JSON.stringify(
13178
13353
  excludeNone(trajectory),
13179
13354
  null,
@@ -13187,7 +13362,11 @@ var NormalizeMiddleware = class {
13187
13362
  metadata: { ...file.metadata, isAtif: true },
13188
13363
  content: Buffer.from(json, "utf-8")
13189
13364
  });
13190
- } catch {
13365
+ } catch (err) {
13366
+ appendLog(
13367
+ "warn",
13368
+ `normalize: failed for source=${file.sourceName} session=${sessionId ?? "<unknown>"} file=${file.absolutePath}: ${err instanceof Error ? err.message : String(err)}`
13369
+ );
13191
13370
  }
13192
13371
  }
13193
13372
  return { ...group, files: newFiles };
@@ -13198,9 +13377,9 @@ var NormalizeMiddleware = class {
13198
13377
  import crypto2 from "crypto";
13199
13378
  import fs10 from "fs";
13200
13379
  import os5 from "os";
13201
- import path13 from "path";
13380
+ import path14 from "path";
13202
13381
  var CURRENT_SCHEMA_VERSION2 = 1;
13203
- var DEFAULT_STATE_DIR = path13.join(
13382
+ var DEFAULT_STATE_DIR = path14.join(
13204
13383
  os5.homedir(),
13205
13384
  ".hillclimb",
13206
13385
  "agent-uploads"
@@ -13224,8 +13403,8 @@ function stateTtlMs() {
13224
13403
  );
13225
13404
  }
13226
13405
  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`);
13406
+ const hash = crypto2.createHash("sha256").update(`${path14.resolve(repoRoot)}\0${tool}\0${sessionId}`).digest("hex").slice(0, 16);
13407
+ return path14.join(stateDir2(), `${hash}.json`);
13229
13408
  }
13230
13409
  function lockFileFor(repoRoot, tool, sessionId) {
13231
13410
  return `${stateFileFor(repoRoot, tool, sessionId)}.lock`;
@@ -13267,8 +13446,11 @@ async function acquireLock2(repoRoot, tool, sessionId, retries = LOCK_RETRIES2,
13267
13446
  lockPath,
13268
13447
  fs10.constants.O_CREAT | fs10.constants.O_EXCL | fs10.constants.O_WRONLY
13269
13448
  );
13270
- await fd.write(String(process.pid));
13271
- await fd.close();
13449
+ try {
13450
+ await fd.write(String(process.pid));
13451
+ } finally {
13452
+ await fd.close();
13453
+ }
13272
13454
  return;
13273
13455
  } catch (err) {
13274
13456
  if (err.code === "EEXIST" && i < retries - 1) {
@@ -13303,7 +13485,7 @@ async function sweepStaleUploadStates(ttlMs = stateTtlMs(), now = Date.now()) {
13303
13485
  }
13304
13486
  for (const entry of entries) {
13305
13487
  if (!entry.isFile()) continue;
13306
- const file = path13.join(stateDir2(), entry.name);
13488
+ const file = path14.join(stateDir2(), entry.name);
13307
13489
  try {
13308
13490
  const st = await fs10.promises.stat(file);
13309
13491
  if (now - st.mtimeMs > ttlMs) {
@@ -13329,6 +13511,9 @@ function sanitize2(value) {
13329
13511
  function formatEpochSeconds2(date) {
13330
13512
  return String(Math.floor(date.getTime() / 1e3));
13331
13513
  }
13514
+ function newFlowId() {
13515
+ return crypto3.randomBytes(3).toString("hex");
13516
+ }
13332
13517
  function lineHasAssistant(line) {
13333
13518
  return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
13334
13519
  }
@@ -13355,16 +13540,41 @@ async function hasAssistantMessage(transcriptPath) {
13355
13540
  }
13356
13541
  return false;
13357
13542
  }
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";
13543
+ function hashFileSha256(file) {
13544
+ return new Promise((resolve, reject) => {
13545
+ const hash = crypto3.createHash("sha256");
13546
+ const stream = fs11.createReadStream(file);
13547
+ stream.on("data", (chunk) => hash.update(chunk));
13548
+ stream.on("error", reject);
13549
+ stream.on("end", () => resolve(hash.digest("hex")));
13550
+ });
13551
+ }
13552
+ async function resolveSourceTool(payload, repoRoot) {
13553
+ const inferred = inferSourceToolFromPayload(payload);
13554
+ if (inferred) return inferred;
13555
+ if (repoRoot) {
13556
+ const owners = await findLegacyUploadHookOwners(
13557
+ repoRoot,
13558
+ payload.hook_event_name
13559
+ );
13560
+ if (owners.length === 1) {
13561
+ appendLog("info", `upload: inferred legacy bare hook owner ${owners[0]}`);
13562
+ return owners[0];
13563
+ }
13564
+ if (owners.length > 1) {
13565
+ appendLog(
13566
+ "warn",
13567
+ `upload: legacy bare hook matched multiple tools (${owners.join(", ")}); skipping to avoid misattribution`
13568
+ );
13569
+ return null;
13570
+ }
13571
+ }
13572
+ return fallbackSourceTool(payload);
13363
13573
  }
13364
13574
  function summarizePayload(payload) {
13365
- const sessionId = payload.session_id ?? payload.conversation_id ?? null;
13575
+ const sessionId = resolveHookSessionId(payload);
13366
13576
  const turnId = payload.turn_id ?? null;
13367
- const cwd = payload.cwd ?? payload.workspace_roots?.[0] ?? null;
13577
+ const cwd = resolveHookCwd(payload);
13368
13578
  return JSON.stringify({
13369
13579
  session_id: sessionId,
13370
13580
  turn_id: turnId,
@@ -13397,11 +13607,11 @@ async function selfHealHook(repoRoot, tool) {
13397
13607
  }
13398
13608
  }
13399
13609
  function resolveCursorTranscriptPath(payload) {
13400
- const id = payload.conversation_id ?? payload.session_id;
13610
+ const id = resolveHookSessionId(payload);
13401
13611
  const workspace = payload.workspace_roots?.[0];
13402
13612
  if (!id || !workspace) return void 0;
13403
13613
  const encoded = workspace.replace(/^\//, "").replace(/\//g, "-");
13404
- return path14.join(
13614
+ return path15.join(
13405
13615
  os6.homedir(),
13406
13616
  ".cursor",
13407
13617
  "projects",
@@ -13412,15 +13622,10 @@ function resolveCursorTranscriptPath(payload) {
13412
13622
  );
13413
13623
  }
13414
13624
  async function runUploadInner(payload) {
13415
- const sessionId = payload.session_id;
13625
+ const sessionId = resolveHookSessionId(payload);
13416
13626
  const transcriptPath = payload.transcript_path ?? resolveCursorTranscriptPath(payload);
13417
- const cwd = payload.cwd ?? payload.workspace_roots?.[0];
13418
- const sourceTool = resolveSourceTool(payload);
13627
+ const cwd = resolveHookCwd(payload);
13419
13628
  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
13629
  if (!sessionId || !transcriptPath || !cwd) {
13425
13630
  appendLog(
13426
13631
  "warn",
@@ -13437,8 +13642,18 @@ async function runUploadInner(payload) {
13437
13642
  return;
13438
13643
  }
13439
13644
  const { repoRoot, config } = match;
13645
+ const sourceTool = await resolveSourceTool(payload, repoRoot);
13646
+ if (!sourceTool) {
13647
+ payload.tool = "unknown";
13648
+ return;
13649
+ }
13650
+ payload.tool = sourceTool;
13651
+ appendLog(
13652
+ "info",
13653
+ `[${sessionId}] payload parsed (tool=${sourceTool}, cwd=${cwd}, event=${eventKind ?? "?"})`
13654
+ );
13440
13655
  await selfHealHook(repoRoot, sourceTool);
13441
- const transcriptResolved = path14.resolve(transcriptPath);
13656
+ const transcriptResolved = path15.resolve(transcriptPath);
13442
13657
  try {
13443
13658
  const stat = await fs11.promises.stat(transcriptResolved);
13444
13659
  if (!stat.isFile()) {
@@ -13477,11 +13692,13 @@ async function uploadSession(args) {
13477
13692
  await withUploadLock(repoRoot, sourceTool, sessionId, async () => {
13478
13693
  const prior = await readUploadState(repoRoot, sourceTool, sessionId);
13479
13694
  let transcriptSize;
13695
+ let transcriptSha256;
13480
13696
  try {
13481
13697
  transcriptSize = (await fs11.promises.stat(transcriptPath)).size;
13698
+ transcriptSha256 = await hashFileSha256(transcriptPath);
13482
13699
  } catch {
13483
13700
  }
13484
- if (prior?.contributionId && transcriptSize !== void 0 && transcriptSize === prior.lastTranscriptSize) {
13701
+ if (prior?.contributionId && transcriptSize !== void 0 && transcriptSha256 !== void 0 && transcriptSize === prior.lastTranscriptSize && transcriptSha256 === prior.lastTranscriptSha256) {
13485
13702
  appendLog(
13486
13703
  "info",
13487
13704
  `[${sessionId}] skipping ${sourceTool} ${isSessionEnd ? "SessionEnd" : "Stop"} upload (transcript unchanged at ${transcriptSize} bytes${isSessionEnd ? "; local state cleared" : ""})`
@@ -13499,13 +13716,13 @@ async function uploadSession(args) {
13499
13716
  };
13500
13717
  const group = {
13501
13718
  repoPath: repoRoot,
13502
- label: path14.basename(repoRoot),
13719
+ label: path15.basename(repoRoot),
13503
13720
  files: [sourceFile],
13504
13721
  sourceNames: [sourceTool],
13505
13722
  lastModified: now
13506
13723
  };
13507
13724
  const envFileNames = await discoverEnvFiles(repoRoot);
13508
- const envFilePaths = envFileNames.map((n) => path14.join(repoRoot, n));
13725
+ const envFilePaths = envFileNames.map((n) => path15.join(repoRoot, n));
13509
13726
  const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
13510
13727
  const mwChain = [];
13511
13728
  if (secretResult.values.size > 0) {
@@ -13566,7 +13783,8 @@ Uploaded: ${now.toISOString()}`;
13566
13783
  uploadCount: prior?.uploadCount ?? 0,
13567
13784
  firstUploadedAt: prior?.firstUploadedAt ?? now.toISOString(),
13568
13785
  lastUploadedAt: prior?.lastUploadedAt ?? now.toISOString(),
13569
- lastTranscriptSize: prior?.lastTranscriptSize
13786
+ lastTranscriptSize: prior?.lastTranscriptSize,
13787
+ lastTranscriptSha256: prior?.lastTranscriptSha256
13570
13788
  })
13571
13789
  });
13572
13790
  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)";
@@ -13604,7 +13822,8 @@ Uploaded: ${now.toISOString()}`;
13604
13822
  uploadCount: seq,
13605
13823
  firstUploadedAt: prior?.firstUploadedAt ?? now.toISOString(),
13606
13824
  lastUploadedAt: now.toISOString(),
13607
- lastTranscriptSize: transcriptSize
13825
+ lastTranscriptSize: transcriptSize,
13826
+ lastTranscriptSha256: transcriptSha256
13608
13827
  };
13609
13828
  await writeUploadState(next);
13610
13829
  appendLog(
@@ -13622,6 +13841,7 @@ Uploaded: ${now.toISOString()}`;
13622
13841
  }
13623
13842
  var WORKER_ENV_FLAG = "HILLCLIMB_UPLOAD_WORKER";
13624
13843
  var TOOL_ENV_FLAG = "HILLCLIMB_UPLOAD_TOOL";
13844
+ var FLOW_ID_ENV = "HILLCLIMB_UPLOAD_FLOW";
13625
13845
  function parseToolArg(argv) {
13626
13846
  for (let i = 0; i < argv.length; i++) {
13627
13847
  const a = argv[i];
@@ -13635,6 +13855,8 @@ async function runUpload() {
13635
13855
  await runUploadWorker();
13636
13856
  return;
13637
13857
  }
13858
+ const flowId = newFlowId();
13859
+ setLogPrefix(`[${flowId}]`);
13638
13860
  appendLog("info", `upload hook invoked (pid ${process.pid})`);
13639
13861
  let raw;
13640
13862
  try {
@@ -13661,7 +13883,8 @@ async function runUpload() {
13661
13883
  const toolArg = parseToolArg(process.argv.slice(2));
13662
13884
  const workerEnv = {
13663
13885
  ...process.env,
13664
- [WORKER_ENV_FLAG]: "1"
13886
+ [WORKER_ENV_FLAG]: "1",
13887
+ [FLOW_ID_ENV]: flowId
13665
13888
  };
13666
13889
  if (toolArg) workerEnv[TOOL_ENV_FLAG] = toolArg;
13667
13890
  try {
@@ -13688,6 +13911,7 @@ async function runUpload() {
13688
13911
  }
13689
13912
  }
13690
13913
  async function runUploadWorker() {
13914
+ setLogPrefix(`[${process.env[FLOW_ID_ENV] ?? newFlowId()}]`);
13691
13915
  appendLog("info", `upload worker started (pid ${process.pid})`);
13692
13916
  let raw;
13693
13917
  try {
@@ -13726,7 +13950,7 @@ async function runUploadWorker() {
13726
13950
  } finally {
13727
13951
  await recordDebugLogCompletion({
13728
13952
  kind: "agent",
13729
- tool: resolveSourceTool(payload),
13953
+ tool: fallbackSourceTool(payload),
13730
13954
  payload
13731
13955
  });
13732
13956
  try {
@@ -13738,17 +13962,17 @@ async function runUploadWorker() {
13738
13962
 
13739
13963
  // src/git-traces/index.ts
13740
13964
  import { spawn as spawn3 } from "child_process";
13741
- import crypto4 from "crypto";
13965
+ import crypto5 from "crypto";
13742
13966
 
13743
13967
  // src/git-traces/handlers.ts
13744
- import { execFileSync as execFileSync2 } from "child_process";
13745
- import path17 from "path";
13968
+ import { execFileSync as execFileSync3 } from "child_process";
13969
+ import path18 from "path";
13746
13970
 
13747
13971
  // src/git-traces/git-ops.ts
13748
- import { execFileSync } from "child_process";
13972
+ import { execFileSync as execFileSync2 } from "child_process";
13749
13973
  import fs12 from "fs";
13750
13974
  import os7 from "os";
13751
- import path15 from "path";
13975
+ import path16 from "path";
13752
13976
  import { gzipSync } from "zlib";
13753
13977
  var GIT_COMMAND_TIMEOUT_MS = 12e4;
13754
13978
  var EXEC_OPTS = {
@@ -13802,7 +14026,7 @@ function isGitTimeoutError(err) {
13802
14026
  }
13803
14027
  function gitBuffer(repoRoot, args, options = {}) {
13804
14028
  try {
13805
- return execFileSync("git", args, {
14029
+ return execFileSync2("git", args, {
13806
14030
  cwd: repoRoot,
13807
14031
  ...options.env ? { env: options.env } : {},
13808
14032
  ...options.input !== void 0 ? { input: options.input } : {},
@@ -13911,7 +14135,7 @@ function removePathsFromIndex(repoRoot, env, paths) {
13911
14135
  function filterOversizedFilesFromTree(repoRoot, treeSha, options = {}) {
13912
14136
  const oversizedFiles = listOversizedTreeFiles(repoRoot, treeSha, options);
13913
14137
  if (oversizedFiles.length === 0) return treeSha;
13914
- const tmpIndex = path15.join(
14138
+ const tmpIndex = path16.join(
13915
14139
  os7.tmpdir(),
13916
14140
  `hillclimb-filter-${Date.now()}-${process.pid}`
13917
14141
  );
@@ -13943,7 +14167,7 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
13943
14167
  for (const relPath of list.split("\0")) {
13944
14168
  if (!relPath) continue;
13945
14169
  try {
13946
- const stat = fs12.lstatSync(path15.join(repoRoot, relPath));
14170
+ const stat = fs12.lstatSync(path16.join(repoRoot, relPath));
13947
14171
  if (stat.size > MAX_SNAPSHOT_FILE_BYTES) {
13948
14172
  recordOmittedSnapshotFile(omittedFiles, {
13949
14173
  path: relPath,
@@ -13958,7 +14182,7 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
13958
14182
  }
13959
14183
  }
13960
14184
  if (kept.length === 0) return null;
13961
- const tmpIndex = path15.join(
14185
+ const tmpIndex = path16.join(
13962
14186
  os7.tmpdir(),
13963
14187
  `hillclimb-untracked-${Date.now()}-${process.pid}`
13964
14188
  );
@@ -13988,7 +14212,7 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
13988
14212
  const untrackedTree = buildUntrackedTree(repoRoot, options.omittedFiles);
13989
14213
  if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA)
13990
14214
  return filteredTrackedTree;
13991
- const tmpIndex = path15.join(
14215
+ const tmpIndex = path16.join(
13992
14216
  os7.tmpdir(),
13993
14217
  `hillclimb-index-${Date.now()}-${process.pid}`
13994
14218
  );
@@ -14030,7 +14254,7 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
14030
14254
  ]);
14031
14255
  const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
14032
14256
  pinRef(repoRoot, orphanRef, orphanCommit);
14033
- const tmpFile = path15.join(
14257
+ const tmpFile = path16.join(
14034
14258
  os7.tmpdir(),
14035
14259
  // Include the pid (like the other temp files in this module) so concurrent
14036
14260
  // git-traces workers — e.g. two sessions, or a parent + subagent — don't
@@ -14229,9 +14453,9 @@ function parseCommitFiles(repoRoot, sha) {
14229
14453
  oldPath
14230
14454
  });
14231
14455
  } else {
14232
- const path24 = parts[parts.length - 1];
14233
- indexByPath.set(path24, files.length);
14234
- files.push({ path: path24, status, additions: 0, deletions: 0 });
14456
+ const path25 = parts[parts.length - 1];
14457
+ indexByPath.set(path25, files.length);
14458
+ files.push({ path: path25, status, additions: 0, deletions: 0 });
14235
14459
  }
14236
14460
  }
14237
14461
  for (const line of numstat.split("\n")) {
@@ -14297,22 +14521,22 @@ function cleanupSessionRefs(repoRoot, sessionId) {
14297
14521
  }
14298
14522
 
14299
14523
  // src/git-traces/session-state.ts
14300
- import crypto3 from "crypto";
14524
+ import crypto4 from "crypto";
14301
14525
  import fs13 from "fs";
14302
14526
  import os8 from "os";
14303
- import path16 from "path";
14527
+ import path17 from "path";
14304
14528
  var CURRENT_SCHEMA_VERSION3 = 3;
14305
- var DEFAULT_STATE_DIR2 = path16.join(os8.homedir(), ".hillclimb", "git-traces");
14529
+ var DEFAULT_STATE_DIR2 = path17.join(os8.homedir(), ".hillclimb", "git-traces");
14306
14530
  var LOCK_RETRIES3 = 120;
14307
14531
  var LOCK_RETRY_DELAY_MS3 = 500;
14308
14532
  function stateDir3() {
14309
14533
  return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR2;
14310
14534
  }
14311
14535
  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}`
14536
+ const hash = crypto4.createHash("sha256").update(
14537
+ sessionId ? `${path17.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path17.resolve(repoRoot)}\0${tool}`
14314
14538
  ).digest("hex").slice(0, 16);
14315
- return path16.join(stateDir3(), `${hash}.json`);
14539
+ return path17.join(stateDir3(), `${hash}.json`);
14316
14540
  }
14317
14541
  function lockFileForRepo(repoRoot, tool) {
14318
14542
  return `${stateFileForRepo(repoRoot, tool)}.lock`;
@@ -14339,14 +14563,14 @@ async function listScopedSessionStates(repoRoot, tool) {
14339
14563
  const states = [];
14340
14564
  for (const entry of entries) {
14341
14565
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
14342
- const file = path16.join(stateDir3(), entry.name);
14566
+ const file = path17.join(stateDir3(), entry.name);
14343
14567
  const state = await readStateFile(file);
14344
14568
  if (!state) continue;
14345
14569
  if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
14346
14570
  continue;
14347
14571
  }
14348
- if (path16.resolve(state.repoRoot) !== path16.resolve(repoRoot)) continue;
14349
- if (path16.resolve(file) !== path16.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
14572
+ if (path17.resolve(state.repoRoot) !== path17.resolve(repoRoot)) continue;
14573
+ if (path17.resolve(file) !== path17.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
14350
14574
  continue;
14351
14575
  }
14352
14576
  let mtimeMs = 0;
@@ -14369,14 +14593,14 @@ async function listSessionStatesForSession(tool, sessionId) {
14369
14593
  const states = [];
14370
14594
  for (const entry of entries) {
14371
14595
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
14372
- const file = path16.join(stateDir3(), entry.name);
14596
+ const file = path17.join(stateDir3(), entry.name);
14373
14597
  const state = await readStateFile(file);
14374
14598
  if (!state) continue;
14375
14599
  if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
14376
14600
  continue;
14377
14601
  }
14378
14602
  if (state.sessionId !== sessionId) continue;
14379
- if (path16.resolve(file) !== path16.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
14603
+ if (path17.resolve(file) !== path17.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
14380
14604
  continue;
14381
14605
  }
14382
14606
  let mtimeMs = 0;
@@ -14479,18 +14703,18 @@ var TOOL_LABELS = {
14479
14703
  async function loadConfiguredRepos() {
14480
14704
  const file = await loadProjects();
14481
14705
  return Object.entries(file.projects).map(([repoRoot, config]) => ({
14482
- repoRoot: path17.resolve(repoRoot),
14706
+ repoRoot: path18.resolve(repoRoot),
14483
14707
  config
14484
14708
  })).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
14485
14709
  }
14486
14710
  function repoLabel(repoRoot) {
14487
- return path17.basename(repoRoot) || repoRoot;
14711
+ return path18.basename(repoRoot) || repoRoot;
14488
14712
  }
14489
14713
  function resolveCwd2(payload) {
14490
- return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
14714
+ return resolveHookCwd(payload);
14491
14715
  }
14492
14716
  function resolveSessionId2(payload) {
14493
- return payload.session_id ?? payload.conversation_id ?? null;
14717
+ return resolveHookSessionId(payload);
14494
14718
  }
14495
14719
  function epochPrefix(epoch) {
14496
14720
  return `epoch-${String(epoch).padStart(3, "0")}`;
@@ -14506,7 +14730,7 @@ function captureHeadSha(cwd) {
14506
14730
  }
14507
14731
  }
14508
14732
  function execGit(cwd, args) {
14509
- return execFileSync2("git", args, {
14733
+ return execFileSync3("git", args, {
14510
14734
  cwd,
14511
14735
  stdio: ["pipe", "pipe", "pipe"],
14512
14736
  timeout: 3e4,
@@ -14699,27 +14923,34 @@ async function createGitTracesContribution(params) {
14699
14923
  const epochSeconds = formatEpochSeconds3(now);
14700
14924
  const shortId = state.sessionId.slice(0, 12);
14701
14925
  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}
14926
+ let contributionId = state.contributionId;
14927
+ if (!contributionId) {
14928
+ const contribution = await client.createContribution(config.projectId, {
14929
+ contributionTypeSlug: GIT_TRACES_SLUG,
14930
+ title: `${toolLabel2} session ${shortId} \u2014 ${repoName} \u2014 ${epochSeconds}`,
14931
+ body: `Session ID: ${state.sessionId}
14706
14932
  Tool: ${toolLabel2}
14707
14933
  Repo: ${repoRoot}
14708
14934
  Uploaded: ${now.toISOString()}`
14709
- });
14935
+ });
14936
+ contributionId = contribution.id;
14937
+ state.contributionId = contributionId;
14938
+ state.baselineUploaded = false;
14939
+ await writeSessionState(state, tool);
14940
+ }
14710
14941
  const uploaded = await uploadEpochBaselineArtifacts({
14711
14942
  client,
14712
- contributionId: contribution.id,
14943
+ contributionId,
14713
14944
  epoch: 1,
14714
14945
  artifacts
14715
14946
  });
14716
14947
  if (!uploaded) return null;
14717
- await client.submitContribution(contribution.id);
14948
+ await client.submitContribution(contributionId);
14718
14949
  appendLog(
14719
14950
  "info",
14720
- `git-traces: baseline uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contribution.id}, epoch=1, bundleBytes=${artifacts.bundleBuffer.byteLength}, metadataBytes=${artifacts.metadataBuffer.byteLength})`
14951
+ `git-traces: baseline uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId}, epoch=1, bundleBytes=${artifacts.bundleBuffer.byteLength}, metadataBytes=${artifacts.metadataBuffer.byteLength})`
14721
14952
  );
14722
- return contribution.id;
14953
+ return contributionId;
14723
14954
  }
14724
14955
  async function uploadEpochBaseline(params) {
14725
14956
  const artifacts = buildEpochBaselineArtifacts(params);
@@ -14760,6 +14991,7 @@ async function initializeSession(repoRoot, tool, sessionId) {
14760
14991
  schemaVersion: CURRENT_SCHEMA_VERSION3,
14761
14992
  sessionId,
14762
14993
  contributionId: null,
14994
+ baselineUploaded: false,
14763
14995
  baselineSha: frozen.baselineSha,
14764
14996
  baselineTreeSha: frozen.baselineTreeSha,
14765
14997
  baselineMetadata: frozen.baselineMetadata,
@@ -14920,6 +15152,7 @@ async function registerInitialContribution(params) {
14920
15152
  });
14921
15153
  if (!contributionId) return false;
14922
15154
  state.contributionId = contributionId;
15155
+ state.baselineUploaded = true;
14923
15156
  state.baselineTreeSha = artifacts.baselineTreeSha;
14924
15157
  state.lastSnapshotTreeSha = artifacts.baselineTreeSha;
14925
15158
  await writeSessionState(state, tool);
@@ -14961,7 +15194,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
14961
15194
  if (currentHeadSha !== state.headSha) {
14962
15195
  const client2 = await loadRepoClient(repo);
14963
15196
  if (!client2) return "skipped";
14964
- if (state.contributionId === null) {
15197
+ if (state.contributionId === null || !state.baselineUploaded) {
14965
15198
  const artifacts2 = buildInitialBaselineArtifactsForState(
14966
15199
  repoRoot,
14967
15200
  tool,
@@ -15044,7 +15277,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
15044
15277
  const nextTurnCount = state.turnCount + 1;
15045
15278
  const turnLabel = turnSuffix(nextTurnCount);
15046
15279
  const filename = `${prefix}-${turnLabel}-${recordedAt}.patch.gz`;
15047
- if (state.contributionId === null && !canUploadFile(filename, patchBuffer)) {
15280
+ if ((state.contributionId === null || !state.baselineUploaded) && !canUploadFile(filename, patchBuffer)) {
15048
15281
  appendLog(
15049
15282
  "warn",
15050
15283
  `git-traces: first changed turn skipped before contribution creation (repo=${repoRoot}, project=${config.projectId}, reason=patch-too-large)`
@@ -15053,7 +15286,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
15053
15286
  }
15054
15287
  const client = await loadRepoClient(repo);
15055
15288
  if (!client) return "skipped";
15056
- if (state.contributionId === null) {
15289
+ if (state.contributionId === null || !state.baselineUploaded) {
15057
15290
  const artifacts = buildInitialBaselineArtifactsForState(
15058
15291
  repoRoot,
15059
15292
  tool,
@@ -15118,6 +15351,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
15118
15351
  {
15119
15352
  ...state,
15120
15353
  contributionId: null,
15354
+ baselineUploaded: false,
15121
15355
  lastSnapshotSha: state.baselineSha,
15122
15356
  lastSnapshotTreeSha: state.baselineTreeSha,
15123
15357
  turnCount: 0
@@ -15156,7 +15390,7 @@ async function handleStop(payload, tool) {
15156
15390
  if (sessionId) {
15157
15391
  const storedStates = await listSessionStatesForSession(tool, sessionId);
15158
15392
  for (const { state } of storedStates) {
15159
- const repo = repoByRoot.get(path17.resolve(state.repoRoot));
15393
+ const repo = repoByRoot.get(path18.resolve(state.repoRoot));
15160
15394
  if (!repo) {
15161
15395
  missingConfig++;
15162
15396
  appendLog(
@@ -15218,21 +15452,62 @@ async function handleSessionEnd(payload, tool) {
15218
15452
  const sessionId = resolveSessionId2(payload);
15219
15453
  const project = cwd ? await findProjectForCwd(cwd) : null;
15220
15454
  const triggerRepo = project?.repoRoot ?? cwd ?? "<none>";
15455
+ const recordedAt = Date.now();
15221
15456
  const repoRoots = [];
15222
15457
  if (sessionId) {
15223
15458
  const states = await listSessionStatesForSession(tool, sessionId);
15224
15459
  for (const { state } of states) {
15225
- repoRoots.push(path17.resolve(state.repoRoot));
15460
+ repoRoots.push(path18.resolve(state.repoRoot));
15226
15461
  }
15227
15462
  }
15228
15463
  if (repoRoots.length === 0 && cwd) {
15229
15464
  repoRoots.push(project?.repoRoot ?? cwd);
15230
15465
  }
15231
15466
  if (repoRoots.length === 0) return;
15467
+ const repos = await loadConfiguredRepos();
15468
+ const repoByRoot = new Map(repos.map((repo) => [repo.repoRoot, repo]));
15232
15469
  let cleaned = 0;
15233
15470
  let noState = 0;
15471
+ let uploaded = 0;
15472
+ let unchanged = 0;
15473
+ let skipped = 0;
15234
15474
  let failed = 0;
15235
15475
  for (const repoRoot of repoRoots) {
15476
+ const repo = repoByRoot.get(path18.resolve(repoRoot)) ?? (project && path18.resolve(project.repoRoot) === path18.resolve(repoRoot) ? { repoRoot: project.repoRoot, config: project.config } : null);
15477
+ if (!repo) {
15478
+ skipped++;
15479
+ appendLog(
15480
+ "warn",
15481
+ `git-traces: preserving SessionEnd state for repo ${repoRoot} (reason=missing-config)`
15482
+ );
15483
+ continue;
15484
+ }
15485
+ const finalOutcome = await processStopRepo(
15486
+ repo,
15487
+ tool,
15488
+ sessionId,
15489
+ recordedAt
15490
+ );
15491
+ if (finalOutcome === "uploaded") uploaded++;
15492
+ else if (finalOutcome === "unchanged") unchanged++;
15493
+ else if (finalOutcome === "skipped") {
15494
+ skipped++;
15495
+ appendLog(
15496
+ "warn",
15497
+ `git-traces: preserving SessionEnd state for repo ${repoRoot} after skipped final upload`
15498
+ );
15499
+ continue;
15500
+ } else if (finalOutcome === "failed") {
15501
+ failed++;
15502
+ appendLog(
15503
+ "warn",
15504
+ `git-traces: preserving SessionEnd state for repo ${repoRoot} after failed final upload`
15505
+ );
15506
+ continue;
15507
+ } else {
15508
+ noState++;
15509
+ continue;
15510
+ }
15236
15511
  const outcome = await cleanupSessionStateForRepo(repoRoot, tool, sessionId);
15237
15512
  if (outcome === "cleaned") cleaned++;
15238
15513
  else if (outcome === "no-state") noState++;
@@ -15240,16 +15515,16 @@ async function handleSessionEnd(payload, tool) {
15240
15515
  }
15241
15516
  appendLog(
15242
15517
  "info",
15243
- `git-traces: SessionEnd summary (session=${sessionId ?? "<none>"}, tool=${tool}, triggerRepo=${triggerRepo}, states=${repoRoots.length}, cleaned=${cleaned}, noState=${noState}, failed=${failed})`
15518
+ `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
15519
  );
15245
15520
  }
15246
15521
 
15247
15522
  // src/git-traces/index.ts
15248
15523
  var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
15249
15524
  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");
15525
+ var FLOW_ID_ENV2 = "HILLCLIMB_GIT_TRACES_FLOW";
15526
+ function newFlowId2() {
15527
+ return crypto5.randomBytes(3).toString("hex");
15253
15528
  }
15254
15529
  var KNOWN_TOOLS = /* @__PURE__ */ new Set([
15255
15530
  "claude",
@@ -15388,7 +15663,7 @@ async function runGitTraces() {
15388
15663
  await runGitTracesWorker();
15389
15664
  return;
15390
15665
  }
15391
- const flowId = newFlowId();
15666
+ const flowId = newFlowId2();
15392
15667
  setLogPrefix(`[${flowId}]`);
15393
15668
  const toolArg = parseToolArg2(process.argv.slice(2));
15394
15669
  appendLog(
@@ -15431,7 +15706,7 @@ async function runGitTraces() {
15431
15706
  ...process.env,
15432
15707
  [WORKER_ENV_FLAG2]: "1",
15433
15708
  [TOOL_ENV_FLAG2]: tool,
15434
- [FLOW_ID_ENV]: flowId
15709
+ [FLOW_ID_ENV2]: flowId
15435
15710
  }
15436
15711
  }
15437
15712
  );
@@ -15453,7 +15728,7 @@ async function runGitTraces() {
15453
15728
  }
15454
15729
  }
15455
15730
  async function runGitTracesWorker() {
15456
- setLogPrefix(`[${process.env[FLOW_ID_ENV] ?? newFlowId()}]`);
15731
+ setLogPrefix(`[${process.env[FLOW_ID_ENV2] ?? newFlowId2()}]`);
15457
15732
  const tool = process.env[TOOL_ENV_FLAG2] ?? parseToolArg2(process.argv.slice(2)) ?? null;
15458
15733
  appendLog(
15459
15734
  "info",
@@ -15536,14 +15811,14 @@ ${stack}` : ""}`
15536
15811
 
15537
15812
  // src/outputs/zip.ts
15538
15813
  import fs15 from "fs";
15539
- import path19 from "path";
15814
+ import path20 from "path";
15540
15815
  import archiver2 from "archiver";
15541
15816
 
15542
15817
  // src/outputs/downloads.ts
15543
15818
  import { execSync as execSync2 } from "child_process";
15544
15819
  import fs14 from "fs";
15545
15820
  import os9 from "os";
15546
- import path18 from "path";
15821
+ import path19 from "path";
15547
15822
  function getDownloadsFolder() {
15548
15823
  const home = os9.homedir();
15549
15824
  if (process.platform === "linux") {
@@ -15556,7 +15831,7 @@ function getDownloadsFolder() {
15556
15831
  } catch {
15557
15832
  }
15558
15833
  }
15559
- const downloads = path18.join(home, "Downloads");
15834
+ const downloads = path19.join(home, "Downloads");
15560
15835
  if (fs14.existsSync(downloads)) return downloads;
15561
15836
  return home;
15562
15837
  }
@@ -15566,11 +15841,11 @@ function sanitizeFilename(name) {
15566
15841
  return name.replace(/[^a-zA-Z0-9._-]/g, "_");
15567
15842
  }
15568
15843
  function getUniqueFilename(dir, base, ext) {
15569
- let candidate = path19.join(dir, `${base}${ext}`);
15844
+ let candidate = path20.join(dir, `${base}${ext}`);
15570
15845
  if (!fs15.existsSync(candidate)) return candidate;
15571
15846
  let i = 1;
15572
15847
  while (fs15.existsSync(candidate)) {
15573
- candidate = path19.join(dir, `${base}-${i}${ext}`);
15848
+ candidate = path20.join(dir, `${base}-${i}${ext}`);
15574
15849
  i++;
15575
15850
  }
15576
15851
  return candidate;
@@ -15580,7 +15855,7 @@ var ZipOutput = class {
15580
15855
  label = "Save as .zip to Downloads";
15581
15856
  async emit(group, options) {
15582
15857
  const downloadsDir = getDownloadsFolder();
15583
- const repoName = sanitizeFilename(path19.basename(group.repoPath));
15858
+ const repoName = sanitizeFilename(path20.basename(group.repoPath));
15584
15859
  const timeRange = options.timeRange;
15585
15860
  const rangePart = timeRange?.label ?? "all";
15586
15861
  const epochSeconds = Math.floor(Date.now() / 1e3);
@@ -15782,11 +16057,11 @@ async function confirmExport(group, output) {
15782
16057
  // src/sources/claude.ts
15783
16058
  import fs16 from "fs";
15784
16059
  import os10 from "os";
15785
- import path20 from "path";
16060
+ import path21 from "path";
15786
16061
  import readline from "readline";
15787
16062
  var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
15788
16063
  async function resolveRepoPath(projectDir) {
15789
- const indexPath = path20.join(projectDir, "sessions-index.json");
16064
+ const indexPath = path21.join(projectDir, "sessions-index.json");
15790
16065
  try {
15791
16066
  const raw = await fs16.promises.readFile(indexPath, "utf-8");
15792
16067
  const data = JSON.parse(raw);
@@ -15801,7 +16076,7 @@ async function resolveRepoPath(projectDir) {
15801
16076
  });
15802
16077
  for (const entry of entries) {
15803
16078
  if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
15804
- const cwd = await extractCwdFromJsonl(path20.join(projectDir, entry.name));
16079
+ const cwd = await extractCwdFromJsonl(path21.join(projectDir, entry.name));
15805
16080
  if (cwd) {
15806
16081
  cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
15807
16082
  }
@@ -15847,7 +16122,7 @@ async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
15847
16122
  return;
15848
16123
  }
15849
16124
  for (const entry of entries) {
15850
- const fullPath = path20.join(dir, entry.name);
16125
+ const fullPath = path21.join(dir, entry.name);
15851
16126
  if (entry.isDirectory()) {
15852
16127
  if (SKIP_DIRS.has(entry.name)) continue;
15853
16128
  await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
@@ -15869,7 +16144,7 @@ function fallbackDecode(encodedName) {
15869
16144
  var ClaudeSource = class {
15870
16145
  name = "claude";
15871
16146
  async scan() {
15872
- const baseDir = path20.join(os10.homedir(), ".claude", "projects");
16147
+ const baseDir = path21.join(os10.homedir(), ".claude", "projects");
15873
16148
  try {
15874
16149
  await fs16.promises.access(baseDir);
15875
16150
  } catch {
@@ -15881,7 +16156,7 @@ var ClaudeSource = class {
15881
16156
  const dirEntries = projectDirs.filter((d) => d.isDirectory());
15882
16157
  const resultArrays = await Promise.all(
15883
16158
  dirEntries.map(async (dir) => {
15884
- const projectPath = path20.join(baseDir, dir.name);
16159
+ const projectPath = path21.join(baseDir, dir.name);
15885
16160
  const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
15886
16161
  const files = [];
15887
16162
  await collectFiles(
@@ -15901,7 +16176,7 @@ var ClaudeSource = class {
15901
16176
  // src/sources/codex.ts
15902
16177
  import fs17 from "fs";
15903
16178
  import os11 from "os";
15904
- import path21 from "path";
16179
+ import path22 from "path";
15905
16180
  import readline2 from "readline";
15906
16181
  async function parseSessionMeta(filePath) {
15907
16182
  const stream = fs17.createReadStream(filePath, { encoding: "utf-8" });
@@ -15934,7 +16209,7 @@ async function findJsonlFiles(dir) {
15934
16209
  return;
15935
16210
  }
15936
16211
  for (const entry of entries) {
15937
- const full = path21.join(d, entry.name);
16212
+ const full = path22.join(d, entry.name);
15938
16213
  if (entry.isDirectory()) {
15939
16214
  await walk(full);
15940
16215
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -15979,14 +16254,14 @@ async function loadHistory(historyPath) {
15979
16254
  var CodexSource = class {
15980
16255
  name = "codex";
15981
16256
  async scan() {
15982
- const codexDir = path21.join(os11.homedir(), ".codex");
15983
- const sessionsDir = path21.join(codexDir, "sessions");
16257
+ const codexDir = path22.join(os11.homedir(), ".codex");
16258
+ const sessionsDir = path22.join(codexDir, "sessions");
15984
16259
  try {
15985
16260
  await fs17.promises.access(sessionsDir);
15986
16261
  } catch {
15987
16262
  return [];
15988
16263
  }
15989
- const historyPath = path21.join(codexDir, "history.jsonl");
16264
+ const historyPath = path22.join(codexDir, "history.jsonl");
15990
16265
  const [jsonlFiles, historyMap] = await Promise.all([
15991
16266
  findJsonlFiles(sessionsDir),
15992
16267
  loadHistory(historyPath)
@@ -16009,8 +16284,8 @@ var CodexSource = class {
16009
16284
  });
16010
16285
  const historyLines = historyMap.get(meta.sessionId);
16011
16286
  if (historyLines) {
16012
- const sessionDir = path21.relative(sessionsDir, path21.dirname(filePath));
16013
- const historyAbsPath = path21.join(
16287
+ const sessionDir = path22.relative(sessionsDir, path22.dirname(filePath));
16288
+ const historyAbsPath = path22.join(
16014
16289
  sessionsDir,
16015
16290
  sessionDir,
16016
16291
  `history-${meta.sessionId}.jsonl`
@@ -16032,16 +16307,16 @@ var CodexSource = class {
16032
16307
  // src/sources/copilotChat.ts
16033
16308
  import fs18 from "fs";
16034
16309
  import os12 from "os";
16035
- import path22 from "path";
16310
+ import path23 from "path";
16036
16311
  import { fileURLToPath } from "url";
16037
16312
  function vsCodeUserDirs() {
16038
16313
  const home = os12.homedir();
16039
16314
  const dirs = [
16040
- path22.join(home, "Library", "Application Support", "Code", "User"),
16041
- path22.join(home, ".config", "Code", "User")
16315
+ path23.join(home, "Library", "Application Support", "Code", "User"),
16316
+ path23.join(home, ".config", "Code", "User")
16042
16317
  ];
16043
16318
  if (process.env.APPDATA) {
16044
- dirs.push(path22.join(process.env.APPDATA, "Code", "User"));
16319
+ dirs.push(path23.join(process.env.APPDATA, "Code", "User"));
16045
16320
  }
16046
16321
  return dirs;
16047
16322
  }
@@ -16078,7 +16353,7 @@ var CopilotChatSource = class {
16078
16353
  async scan() {
16079
16354
  const results = [];
16080
16355
  for (const userDir of vsCodeUserDirs()) {
16081
- const workspaceStorage = path22.join(userDir, "workspaceStorage");
16356
+ const workspaceStorage = path23.join(userDir, "workspaceStorage");
16082
16357
  let hashDirs;
16083
16358
  try {
16084
16359
  hashDirs = await fs18.promises.readdir(workspaceStorage, {
@@ -16089,8 +16364,8 @@ var CopilotChatSource = class {
16089
16364
  }
16090
16365
  for (const hash of hashDirs) {
16091
16366
  if (!hash.isDirectory()) continue;
16092
- const wsRoot = path22.join(workspaceStorage, hash.name);
16093
- const transcriptsDir = path22.join(
16367
+ const wsRoot = path23.join(workspaceStorage, hash.name);
16368
+ const transcriptsDir = path23.join(
16094
16369
  wsRoot,
16095
16370
  "GitHub.copilot-chat",
16096
16371
  "transcripts"
@@ -16104,7 +16379,7 @@ var CopilotChatSource = class {
16104
16379
  continue;
16105
16380
  }
16106
16381
  const repoPath = await readWorkspaceFolder(
16107
- path22.join(wsRoot, "workspace.json")
16382
+ path23.join(wsRoot, "workspace.json")
16108
16383
  );
16109
16384
  if (!repoPath) continue;
16110
16385
  for (const entry of transcriptEntries) {
@@ -16112,7 +16387,7 @@ var CopilotChatSource = class {
16112
16387
  const sessionId = entry.name.slice(0, -".jsonl".length);
16113
16388
  results.push({
16114
16389
  sourceName: this.name,
16115
- absolutePath: path22.join(transcriptsDir, entry.name),
16390
+ absolutePath: path23.join(transcriptsDir, entry.name),
16116
16391
  repoPath,
16117
16392
  metadata: { sessionId }
16118
16393
  });
@@ -16179,10 +16454,10 @@ async function runInteractive() {
16179
16454
  s.start(`Scanning ${source.name} logs...`);
16180
16455
  const allFiles = await source.scan();
16181
16456
  const allGroups = await mergeByRepo(allFiles);
16182
- const repoRoot = path23.resolve(repo.root);
16457
+ const repoRoot = path24.resolve(repo.root);
16183
16458
  const matching = allGroups.filter((g) => {
16184
- const resolved = path23.resolve(g.repoPath);
16185
- return resolved === repoRoot || resolved.startsWith(repoRoot + path23.sep);
16459
+ const resolved = path24.resolve(g.repoPath);
16460
+ return resolved === repoRoot || resolved.startsWith(repoRoot + path24.sep);
16186
16461
  });
16187
16462
  if (matching.length === 0) {
16188
16463
  s.stop(`No ${source.name} logs found for ${repo.name}.`);
@@ -16213,7 +16488,7 @@ async function runInteractive() {
16213
16488
  }
16214
16489
  }
16215
16490
  const envFileNames = await discoverEnvFiles(repoRoot);
16216
- const envFilePaths = envFileNames.map((n) => path23.join(repoRoot, n));
16491
+ const envFilePaths = envFileNames.map((n) => path24.join(repoRoot, n));
16217
16492
  const additionalFiles = await promptSecretFiles(envFileNames);
16218
16493
  const secretResult = await collectSecrets(
16219
16494
  repoRoot,