hillclimb 0.2.0 → 0.4.0

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 +1857 -725
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import fs17 from "fs";
5
- import path20 from "path";
4
+ import fs18 from "fs";
5
+ import path22 from "path";
6
6
  import * as p6 from "@clack/prompts";
7
7
 
8
8
  // src/commands/init.ts
@@ -28,8 +28,7 @@ var cyan = wrap(36, 39);
28
28
  import fs from "fs";
29
29
  import os from "os";
30
30
  import path from "path";
31
- var CONFIG_DIR = path.join(os.homedir(), ".hillclimb");
32
- var CONFIG_PATH = path.join(CONFIG_DIR, "projects.json");
31
+ var DEFAULT_CONFIG_DIR = path.join(os.homedir(), ".hillclimb");
33
32
  function normalizeProjectConfig(raw) {
34
33
  if (!raw || typeof raw !== "object") return null;
35
34
  const config = raw;
@@ -54,14 +53,14 @@ function normalizeProjectConfig(raw) {
54
53
  };
55
54
  }
56
55
  function configDir() {
57
- return CONFIG_DIR;
56
+ return process.env.HILLCLIMB_CONFIG_DIR ?? DEFAULT_CONFIG_DIR;
58
57
  }
59
58
  function configPath() {
60
- return CONFIG_PATH;
59
+ return path.join(configDir(), "projects.json");
61
60
  }
62
61
  async function loadProjects() {
63
62
  try {
64
- const raw = await fs.promises.readFile(CONFIG_PATH, "utf-8");
63
+ const raw = await fs.promises.readFile(configPath(), "utf-8");
65
64
  const parsed = JSON.parse(raw);
66
65
  if (!parsed.projects || typeof parsed.projects !== "object") {
67
66
  return { projects: {} };
@@ -77,12 +76,12 @@ async function loadProjects() {
77
76
  }
78
77
  }
79
78
  async function saveProjects(file) {
80
- await fs.promises.mkdir(CONFIG_DIR, { recursive: true, mode: 448 });
81
- const tmp = `${CONFIG_PATH}.tmp`;
79
+ await fs.promises.mkdir(configDir(), { recursive: true, mode: 448 });
80
+ const tmp = `${configPath()}.tmp`;
82
81
  await fs.promises.writeFile(tmp, JSON.stringify(file, null, 2), {
83
82
  mode: 384
84
83
  });
85
- await fs.promises.rename(tmp, CONFIG_PATH);
84
+ await fs.promises.rename(tmp, configPath());
86
85
  }
87
86
  async function upsertProject(repoRoot, config) {
88
87
  const file = await loadProjects();
@@ -177,13 +176,15 @@ async function ensureGitignored(repoRoot, files) {
177
176
  // src/identity.ts
178
177
  import fs3 from "fs";
179
178
  import path4 from "path";
180
- var IDENTITY_PATH = path4.join(configDir(), "identity.json");
179
+ function identityPath() {
180
+ return path4.join(configDir(), "identity.json");
181
+ }
181
182
  function normalizeUrl(apiBaseUrl) {
182
183
  return apiBaseUrl.replace(/\/$/, "");
183
184
  }
184
185
  async function loadAllIdentities() {
185
186
  try {
186
- const raw = await fs3.promises.readFile(IDENTITY_PATH, "utf-8");
187
+ const raw = await fs3.promises.readFile(identityPath(), "utf-8");
187
188
  const parsed = JSON.parse(raw);
188
189
  if (!parsed.identities || typeof parsed.identities !== "object") {
189
190
  return { identities: {} };
@@ -199,11 +200,11 @@ async function loadIdentity(apiBaseUrl) {
199
200
  }
200
201
  async function writeIdentityFile(file) {
201
202
  await fs3.promises.mkdir(configDir(), { recursive: true, mode: 448 });
202
- const tmp = `${IDENTITY_PATH}.tmp`;
203
+ const tmp = `${identityPath()}.tmp`;
203
204
  await fs3.promises.writeFile(tmp, JSON.stringify(file, null, 2), {
204
205
  mode: 384
205
206
  });
206
- await fs3.promises.rename(tmp, IDENTITY_PATH);
207
+ await fs3.promises.rename(tmp, identityPath());
207
208
  }
208
209
  async function saveIdentity(identity) {
209
210
  const file = await loadAllIdentities();
@@ -602,6 +603,8 @@ import path6 from "path";
602
603
  var HOOK_CMD = (sub) => `npx hillclimb@latest ${sub}`;
603
604
  var GIT_TRACES_CMD = (tool) => `npx hillclimb@latest git-traces --tool=${tool}`;
604
605
  var CLAUDE_SESSIONEND_UPLOAD_TIMEOUT_SECONDS = 30;
606
+ var DEV_HOOK_ENV = "HILLCLIMB_DEV_HOOK=1";
607
+ var SKIP_SELF_HEAL_ENV = "HILLCLIMB_SKIP_HOOK_SELF_HEAL=1";
605
608
  var TOOLS = [
606
609
  {
607
610
  tool: "claude",
@@ -764,17 +767,18 @@ function claudeCheck(settings, eventName, command, options = {}) {
764
767
  const matchers = settings.hooks?.[eventName] ?? [];
765
768
  return claudeHookPresent(matchers, command, options);
766
769
  }
767
- function claudeUninstall(settings, eventName, command) {
770
+ function claudeUninstallMatching(settings, eventName, predicate) {
768
771
  const hooks = settings.hooks;
769
772
  const matchers = hooks?.[eventName];
770
773
  if (!hooks || !matchers || matchers.length === 0) return false;
771
774
  let removedAny = false;
772
775
  const next = [];
773
776
  for (const m of matchers) {
774
- const remaining = (m.hooks ?? []).filter(
775
- (h) => !(h.type === "command" && h.command === command)
776
- );
777
- if (remaining.length !== (m.hooks ?? []).length) removedAny = true;
777
+ const remaining = (m.hooks ?? []).filter((h) => {
778
+ const remove = h.type === "command" && typeof h.command === "string" && predicate(h.command);
779
+ if (remove) removedAny = true;
780
+ return !remove;
781
+ });
778
782
  if (remaining.length > 0) next.push({ ...m, hooks: remaining });
779
783
  }
780
784
  if (!removedAny) return false;
@@ -802,11 +806,11 @@ function cursorCheck(settings, eventName, command) {
802
806
  const entries = settings.hooks?.[eventName] ?? [];
803
807
  return cursorHookPresent(entries, command);
804
808
  }
805
- function cursorUninstall(settings, eventName, command) {
809
+ function cursorUninstallMatching(settings, eventName, predicate) {
806
810
  const hooks = settings.hooks;
807
811
  const entries = hooks?.[eventName];
808
812
  if (!hooks || !entries || entries.length === 0) return false;
809
- const next = entries.filter((e) => e.command !== command);
813
+ const next = entries.filter((e) => !predicate(e.command));
810
814
  if (next.length === entries.length) return false;
811
815
  if (next.length > 0) {
812
816
  hooks[eventName] = next;
@@ -834,12 +838,12 @@ function copilotCheck(settings, eventName, command) {
834
838
  const entries = settings.hooks?.[eventName] ?? [];
835
839
  return copilotHookPresent(entries, command);
836
840
  }
837
- function copilotUninstall(settings, eventName, command) {
841
+ function copilotUninstallMatching(settings, eventName, predicate) {
838
842
  const hooks = settings.hooks;
839
843
  const entries = hooks?.[eventName];
840
844
  if (!hooks || !entries || entries.length === 0) return false;
841
845
  const next = entries.filter(
842
- (e) => !(e.type === "command" && e.command === command)
846
+ (e) => !(e.type === "command" && predicate(e.command))
843
847
  );
844
848
  if (next.length === entries.length) return false;
845
849
  if (next.length > 0) {
@@ -850,7 +854,7 @@ function copilotUninstall(settings, eventName, command) {
850
854
  }
851
855
  return true;
852
856
  }
853
- var OPENCODE_PLUGIN_VERSION = 4;
857
+ var OPENCODE_PLUGIN_VERSION = 6;
854
858
  var OPENCODE_PLUGIN_MARKER = `// HILLCLIMB_OPENCODE_PLUGIN_VERSION=${OPENCODE_PLUGIN_VERSION}`;
855
859
  var OPENCODE_PLUGIN_CONTENT = `${OPENCODE_PLUGIN_MARKER}
856
860
  // Auto-installed by \`npx hillclimb\`. Do not edit manually \u2014 re-running
@@ -952,7 +956,10 @@ export const HillclimbPlugin = async ({ directory }) => ({
952
956
  const type = event && event.type;
953
957
  const props = (event && event.properties) || {};
954
958
  const info = props.info || {};
955
- const sessionID = props.sessionID || info.sessionID || info.id;
959
+ const sessionID =
960
+ props.sessionID ||
961
+ info.sessionID ||
962
+ (type === "message.updated" || type === "message.part.updated" ? undefined : info.id);
956
963
  const cwd = directory || props.directory || info.directory;
957
964
 
958
965
  if (type === "message.updated") {
@@ -990,11 +997,8 @@ export const HillclimbPlugin = async ({ directory }) => ({
990
997
 
991
998
  if (type === "session.deleted" && sessionID) {
992
999
  // User explicitly ended this session. Upload its transcript and drop
993
- // the buffer. We do NOT clean up git-traces here: the user may still
994
- // have other live sessions in this repo, and git-traces state is
995
- // keyed per (repoRoot, tool) \u2014 clearing it here would wipe their
996
- // concurrent session. Cleanup happens at server.instance.disposed
997
- // (or on the next session.created via stale-state detection).
1000
+ // the buffer. git-traces state is session-scoped, so this cleanup will
1001
+ // not wipe another concurrent session in the same repo.
998
1002
  const transcriptPath = writeTranscript(sessionID);
999
1003
  if (transcriptPath) {
1000
1004
  spawnHillclimb("upload", {
@@ -1005,15 +1009,19 @@ export const HillclimbPlugin = async ({ directory }) => ({
1005
1009
  tool: TOOL,
1006
1010
  });
1007
1011
  }
1012
+ spawnHillclimb("git-traces --tool=opencode", {
1013
+ session_id: sessionID,
1014
+ cwd,
1015
+ hook_event_name: "session.deleted",
1016
+ tool: TOOL,
1017
+ });
1008
1018
  sessionMessages.delete(sessionID);
1009
1019
  return;
1010
1020
  }
1011
1021
 
1012
1022
  if (type === "server.instance.disposed") {
1013
1023
  // opencode is shutting down \u2014 flush every buffered session's transcript
1014
- // so nothing gets lost. Then clean up git-traces refs/state. No
1015
- // sessionID on this event, but git-traces state is keyed by
1016
- // (repoRoot, tool) so cwd alone is enough for handleSessionEnd.
1024
+ // so nothing gets lost. Then clean up git-traces refs/state per session.
1017
1025
  for (const sid of sessionMessages.keys()) {
1018
1026
  const transcriptPath = writeTranscript(sid);
1019
1027
  if (transcriptPath) {
@@ -1025,8 +1033,15 @@ export const HillclimbPlugin = async ({ directory }) => ({
1025
1033
  tool: TOOL,
1026
1034
  });
1027
1035
  }
1036
+ spawnHillclimb("git-traces --tool=opencode", {
1037
+ session_id: sid,
1038
+ cwd,
1039
+ hook_event_name: "server.instance.disposed",
1040
+ tool: TOOL,
1041
+ });
1028
1042
  }
1029
1043
  sessionMessages.clear();
1044
+ // Keep a no-session cleanup for pre-session-scoped legacy state.
1030
1045
  spawnHillclimb("git-traces --tool=opencode", {
1031
1046
  cwd,
1032
1047
  hook_event_name: "server.instance.disposed",
@@ -1039,16 +1054,16 @@ export const HillclimbPlugin = async ({ directory }) => ({
1039
1054
 
1040
1055
  export default HillclimbPlugin;
1041
1056
  `;
1042
- async function opencodeInstall(file) {
1057
+ async function opencodeInstall(file, content = OPENCODE_PLUGIN_CONTENT) {
1043
1058
  try {
1044
1059
  const existing = await fs5.promises.readFile(file, "utf-8");
1045
- if (existing === OPENCODE_PLUGIN_CONTENT) {
1060
+ if (existing === content) {
1046
1061
  return { installed: 0, alreadyPresent: 1 };
1047
1062
  }
1048
1063
  } catch {
1049
1064
  }
1050
1065
  await fs5.promises.mkdir(path6.dirname(file), { recursive: true });
1051
- await fs5.promises.writeFile(file, OPENCODE_PLUGIN_CONTENT);
1066
+ await fs5.promises.writeFile(file, content);
1052
1067
  return { installed: 1, alreadyPresent: 0 };
1053
1068
  }
1054
1069
  async function opencodeCheck(file) {
@@ -1079,14 +1094,24 @@ function check(settings, format, eventName, command, options = {}) {
1079
1094
  return claudeCheck(settings, eventName, command, options);
1080
1095
  }
1081
1096
  }
1082
- function uninstall(settings, format, eventName, command) {
1097
+ function commandsForEvent(settings, format, eventName) {
1098
+ switch (format) {
1099
+ case "cursor":
1100
+ return (settings.hooks?.[eventName] ?? []).map((e) => e.command).filter((command) => typeof command === "string");
1101
+ case "copilot":
1102
+ return (settings.hooks?.[eventName] ?? []).filter((e) => e.type === "command").map((e) => e.command).filter((command) => typeof command === "string");
1103
+ default:
1104
+ return (settings.hooks?.[eventName] ?? []).flatMap((matcher) => matcher.hooks ?? []).filter((hook) => hook.type === "command").map((hook) => hook.command).filter((command) => typeof command === "string");
1105
+ }
1106
+ }
1107
+ function uninstallMatching(settings, format, eventName, predicate) {
1083
1108
  switch (format) {
1084
1109
  case "cursor":
1085
- return cursorUninstall(settings, eventName, command);
1110
+ return cursorUninstallMatching(settings, eventName, predicate);
1086
1111
  case "copilot":
1087
- return copilotUninstall(settings, eventName, command);
1112
+ return copilotUninstallMatching(settings, eventName, predicate);
1088
1113
  default:
1089
- return claudeUninstall(settings, eventName, command);
1114
+ return claudeUninstallMatching(settings, eventName, predicate);
1090
1115
  }
1091
1116
  }
1092
1117
  function legacyCommandsFor(command) {
@@ -1107,6 +1132,20 @@ function legacyCommandsFor(command) {
1107
1132
  }
1108
1133
  return legacy;
1109
1134
  }
1135
+ function legacyBareGitTracesCommandsFor(command) {
1136
+ return legacyCommandsFor(command).filter(
1137
+ (legacy) => legacy.endsWith(" git-traces")
1138
+ );
1139
+ }
1140
+ function isHillclimbOwnedCommandFor(command, currentCommand) {
1141
+ if (command === currentCommand || legacyCommandsFor(currentCommand).includes(command)) {
1142
+ return true;
1143
+ }
1144
+ const prefix = "npx hillclimb@latest ";
1145
+ if (!currentCommand.startsWith(prefix)) return false;
1146
+ const subcommand = currentCommand.slice(prefix.length);
1147
+ return command.endsWith(` ${subcommand}`) && (command.includes(DEV_HOOK_ENV) || command.includes(SKIP_SELF_HEAL_ENV) && command.includes("dist/cli.js"));
1148
+ }
1110
1149
  async function installHooksForTool(repoRoot, def) {
1111
1150
  const file = settingsPath(repoRoot, def);
1112
1151
  if (def.format === "opencode") {
@@ -1118,10 +1157,13 @@ async function installHooksForTool(repoRoot, def) {
1118
1157
  let alreadyPresent = 0;
1119
1158
  let mutated = false;
1120
1159
  for (const evt of def.events) {
1121
- for (const legacy of legacyCommandsFor(evt.command)) {
1122
- if (uninstall(settings, def.format, evt.eventName, legacy)) {
1123
- mutated = true;
1124
- }
1160
+ if (uninstallMatching(
1161
+ settings,
1162
+ def.format,
1163
+ evt.eventName,
1164
+ (command) => command !== evt.command && isHillclimbOwnedCommandFor(command, evt.command)
1165
+ )) {
1166
+ mutated = true;
1125
1167
  }
1126
1168
  const added = install(settings, def.format, evt.eventName, evt.command, {
1127
1169
  timeout: evt.timeout
@@ -1157,6 +1199,27 @@ async function checkHooksForTool(repoRoot, def) {
1157
1199
  }
1158
1200
  return { allInstalled: missing.length === 0, settingsFile: file, missing };
1159
1201
  }
1202
+ async function findLegacyGitTracesHookOwners(repoRoot, eventName) {
1203
+ if (!eventName) return [];
1204
+ const owners = [];
1205
+ for (const def of TOOLS) {
1206
+ if (def.format === "opencode") continue;
1207
+ const events = def.events.filter(
1208
+ (evt) => evt.eventName === eventName && evt.command.startsWith("npx hillclimb@latest git-traces --tool=")
1209
+ );
1210
+ if (events.length === 0) continue;
1211
+ const settings = await readJson(settingsPath(repoRoot, def));
1212
+ const commands = new Set(commandsForEvent(settings, def.format, eventName));
1213
+ if (events.some(
1214
+ (evt) => legacyBareGitTracesCommandsFor(evt.command).some(
1215
+ (legacy) => commands.has(legacy)
1216
+ )
1217
+ )) {
1218
+ owners.push(def.tool);
1219
+ }
1220
+ }
1221
+ return owners;
1222
+ }
1160
1223
  function detectTools(repoRoot) {
1161
1224
  return TOOLS.filter((def) => def.detect(repoRoot));
1162
1225
  }
@@ -2086,9 +2149,14 @@ async function runStatus(args = []) {
2086
2149
 
2087
2150
  // src/commands/upload.ts
2088
2151
  import { spawn as spawn2 } from "child_process";
2089
- import fs9 from "fs";
2152
+ import fs10 from "fs";
2090
2153
  import os5 from "os";
2091
- import path12 from "path";
2154
+ import path13 from "path";
2155
+
2156
+ // src/debug-logs.ts
2157
+ import crypto from "crypto";
2158
+ import fs9 from "fs";
2159
+ import path11 from "path";
2092
2160
 
2093
2161
  // src/middleware/pattern-redact.ts
2094
2162
  import os3 from "os";
@@ -10781,145 +10849,716 @@ async function collectSecrets(repoRoot, envFiles, additionalFiles) {
10781
10849
  return { values, sourceFiles, processEnvCount, skippedCount };
10782
10850
  }
10783
10851
 
10784
- // src/normalizer/index.ts
10785
- import path9 from "path";
10852
+ // src/outputs/platform.ts
10853
+ import { PassThrough } from "stream";
10854
+ import archiver from "archiver";
10786
10855
 
10787
- // src/normalizer/claude.ts
10788
- function stringify(value) {
10789
- if (typeof value === "string") return value;
10790
- try {
10791
- return JSON.stringify(value);
10792
- } catch {
10793
- return String(value);
10856
+ // src/outputs/archive.ts
10857
+ import os4 from "os";
10858
+ import path9 from "path";
10859
+ function getSourceBaseDir(sourceName) {
10860
+ const home = os4.homedir();
10861
+ switch (sourceName) {
10862
+ case "claude":
10863
+ return path9.join(home, ".claude", "projects");
10864
+ case "codex":
10865
+ return path9.join(home, ".codex", "sessions");
10866
+ case "debug-logs":
10867
+ return path9.join(configDir(), "logs");
10868
+ default:
10869
+ return home;
10794
10870
  }
10795
10871
  }
10796
- function extractTextReasoningToolUses(content) {
10797
- if (typeof content === "string") {
10798
- return [content.trim(), void 0, []];
10799
- }
10800
- const textParts = [];
10801
- const reasoningParts = [];
10802
- const toolBlocks = [];
10803
- if (Array.isArray(content)) {
10804
- for (const block of content) {
10805
- if (typeof block !== "object" || block === null) {
10806
- textParts.push(stringify(block));
10807
- continue;
10808
- }
10809
- const b = block;
10810
- const blockType = b.type;
10811
- if (blockType === "tool_use") {
10812
- toolBlocks.push(b);
10813
- continue;
10814
- }
10815
- if (blockType === "thinking" || blockType === "reasoning" || blockType === "analysis") {
10816
- const textValue2 = b.text !== void 0 && b.text !== null ? b.text : b.thinking;
10817
- if (typeof textValue2 === "string") {
10818
- reasoningParts.push(textValue2.trim());
10819
- } else {
10820
- reasoningParts.push(stringify(textValue2));
10821
- }
10822
- continue;
10823
- }
10824
- if (blockType === "code" && typeof b.code === "string") {
10825
- textParts.push(b.code);
10826
- continue;
10827
- }
10828
- const textValue = b.text;
10829
- if (typeof textValue === "string") {
10830
- textParts.push(textValue);
10831
- } else {
10832
- textParts.push(stringify(b));
10833
- }
10872
+ function addGroupToArchive(archive, group, selectedSources) {
10873
+ for (const file of group.files) {
10874
+ if (!selectedSources.has(file.sourceName)) continue;
10875
+ const baseDir = getSourceBaseDir(file.sourceName);
10876
+ const relativePath = file.absolutePath.startsWith(baseDir) ? path9.relative(baseDir, file.absolutePath) : path9.basename(file.absolutePath);
10877
+ const archivePath = path9.join(file.sourceName, relativePath);
10878
+ if (file.content) {
10879
+ archive.append(file.content, { name: archivePath });
10880
+ } else {
10881
+ archive.file(file.absolutePath, { name: archivePath });
10834
10882
  }
10835
- } else if (content !== void 0 && content !== null) {
10836
- textParts.push(stringify(content));
10837
10883
  }
10838
- const text3 = textParts.filter((p7) => p7 && p7.trim()).map((p7) => p7.trim()).join("\n\n");
10839
- const reasoning = reasoningParts.filter((p7) => p7 && p7.trim()).map((p7) => p7.trim()).join("\n\n");
10840
- return [text3, reasoning || void 0, toolBlocks];
10841
10884
  }
10842
- function buildMetrics(usage) {
10843
- if (typeof usage !== "object" || usage === null) return void 0;
10844
- const u = usage;
10845
- const cachedTokens = u.cache_read_input_tokens || 0;
10846
- const creation = u.cache_creation_input_tokens || 0;
10847
- const inputTokens = u.input_tokens || 0;
10848
- const promptTokens = inputTokens + cachedTokens + creation;
10849
- const completionTokens = u.output_tokens || 0;
10850
- const extra = {};
10851
- for (const [key, value] of Object.entries(u)) {
10852
- if (key === "input_tokens" || key === "output_tokens") continue;
10853
- extra[key] = value;
10854
- }
10855
- return {
10856
- prompt_tokens: promptTokens,
10857
- completion_tokens: completionTokens,
10858
- cached_tokens: cachedTokens,
10859
- extra: Object.keys(extra).length > 0 ? extra : void 0
10860
- };
10885
+
10886
+ // src/outputs/platform.ts
10887
+ async function buildZipBuffer(group, selectedSources) {
10888
+ const archive = archiver("zip", { zlib: { level: 6 } });
10889
+ const stream = new PassThrough();
10890
+ archive.pipe(stream);
10891
+ const chunks = [];
10892
+ const done = new Promise((resolve, reject) => {
10893
+ stream.on("data", (chunk) => chunks.push(chunk));
10894
+ stream.on("end", resolve);
10895
+ stream.on("error", reject);
10896
+ archive.on("error", reject);
10897
+ });
10898
+ addGroupToArchive(archive, group, selectedSources);
10899
+ await archive.finalize();
10900
+ await done;
10901
+ return Buffer.concat(chunks);
10861
10902
  }
10862
- function formatToolResult(block, toolUseResult) {
10863
- const parts = [];
10864
- const content = block.content;
10865
- if (typeof content === "string") {
10866
- if (content.trim()) parts.push(content.trim());
10867
- } else if (Array.isArray(content)) {
10868
- for (const item of content) {
10869
- const text3 = stringify(item);
10870
- if (text3.trim()) parts.push(text3.trim());
10871
- }
10872
- } else if (content !== void 0 && content !== null && content !== "") {
10873
- parts.push(stringify(content));
10903
+ var PlatformUploadOutput = class {
10904
+ constructor(opts) {
10905
+ this.opts = opts;
10874
10906
  }
10875
- let metadata;
10876
- if (toolUseResult && typeof toolUseResult === "object") {
10877
- metadata = { tool_use_result: toolUseResult };
10878
- const stdout = toolUseResult.stdout;
10879
- const stderr = toolUseResult.stderr;
10880
- const exitCode = toolUseResult.exitCode ?? toolUseResult.exit_code;
10881
- const interrupted = toolUseResult.interrupted;
10882
- const isImage = toolUseResult.isImage;
10883
- const formatted = [];
10884
- if (stdout) formatted.push(`[stdout]
10885
- ${stdout}`.trimEnd());
10886
- if (stderr) formatted.push(`[stderr]
10887
- ${stderr}`.trimEnd());
10888
- if (exitCode !== void 0 && exitCode !== null && exitCode !== 0)
10889
- formatted.push(`[exit_code] ${exitCode}`);
10890
- if (interrupted) formatted.push(`[interrupted] ${interrupted}`);
10891
- if (isImage) formatted.push(`[is_image] ${isImage}`);
10892
- const skipKeys = /* @__PURE__ */ new Set([
10893
- "stdout",
10894
- "stderr",
10895
- "exitCode",
10896
- "exit_code",
10897
- "interrupted",
10898
- "isImage"
10899
- ]);
10900
- const remainingMeta = {};
10901
- for (const [k, v] of Object.entries(toolUseResult)) {
10902
- if (!skipKeys.has(k)) remainingMeta[k] = v;
10903
- }
10904
- if (Object.keys(remainingMeta).length > 0) {
10905
- formatted.push(`[metadata] ${JSON.stringify(remainingMeta)}`);
10906
- }
10907
- if (formatted.length > 0) {
10908
- parts.push(formatted.filter(Boolean).join("\n"));
10907
+ name = "platform";
10908
+ label = "Upload to hillclimb platform";
10909
+ async emit(group, options) {
10910
+ const selectedSources = new Set(options.selectedSources);
10911
+ const buffer = await buildZipBuffer(group, selectedSources);
10912
+ const {
10913
+ client,
10914
+ projectId,
10915
+ contributionTypeSlug,
10916
+ contributionTitle,
10917
+ contributionBody,
10918
+ zipFilename,
10919
+ autoSubmit
10920
+ } = this.opts;
10921
+ const contribution = await client.createContribution(projectId, {
10922
+ contributionTypeSlug,
10923
+ title: contributionTitle,
10924
+ body: contributionBody
10925
+ });
10926
+ const presigned = await client.createUpload(contribution.id, {
10927
+ originalFilename: zipFilename,
10928
+ mimeType: "application/zip",
10929
+ sizeBytes: buffer.byteLength
10930
+ });
10931
+ appendLog(
10932
+ "info",
10933
+ `uploading ${zipFilename} (${buffer.byteLength} bytes) to presigned URL`
10934
+ );
10935
+ await client.uploadToPresignedUrl(
10936
+ presigned.presignedUrl,
10937
+ presigned.headers,
10938
+ buffer
10939
+ );
10940
+ appendLog("info", `PUT to presigned URL succeeded for ${zipFilename}`);
10941
+ if (autoSubmit) {
10942
+ appendLog("info", `submitting contribution ${contribution.id}`);
10943
+ await client.submitContribution(contribution.id);
10944
+ appendLog("info", `contribution ${contribution.id} submitted`);
10909
10945
  }
10946
+ return contribution.id;
10910
10947
  }
10911
- if (block.is_error === true) {
10912
- parts.push("[error] tool reported failure");
10913
- metadata = metadata || {};
10914
- metadata.is_error = true;
10915
- }
10916
- if (metadata !== void 0) {
10917
- if (!("raw_tool_result" in metadata)) {
10918
- metadata.raw_tool_result = block;
10919
- }
10948
+ };
10949
+
10950
+ // src/pipeline.ts
10951
+ import fs8 from "fs";
10952
+ import path10 from "path";
10953
+ function canonicalizePath(p7) {
10954
+ let resolved = path10.resolve(p7);
10955
+ if (resolved.endsWith(path10.sep) && resolved !== path10.sep) {
10956
+ resolved = resolved.slice(0, -1);
10920
10957
  }
10921
- const resultText = parts.filter(Boolean).join("\n\n").trim();
10922
- return [resultText || void 0, metadata];
10958
+ return resolved;
10959
+ }
10960
+ function computeLabel(repoPath, allPaths) {
10961
+ const segments = repoPath.split(path10.sep).filter(Boolean);
10962
+ for (let depth = 1; depth <= segments.length; depth++) {
10963
+ const label = segments.slice(-depth).join("/");
10964
+ const matches = allPaths.filter((p7) => {
10965
+ const s = p7.split(path10.sep).filter(Boolean);
10966
+ return s.slice(-depth).join("/") === label;
10967
+ });
10968
+ if (matches.length === 1) return label;
10969
+ }
10970
+ return repoPath;
10971
+ }
10972
+ async function mergeByRepo(files) {
10973
+ const grouped = /* @__PURE__ */ new Map();
10974
+ for (const file of files) {
10975
+ const key = canonicalizePath(file.repoPath);
10976
+ const existing = grouped.get(key);
10977
+ if (existing) {
10978
+ existing.push({ ...file, repoPath: key });
10979
+ } else {
10980
+ grouped.set(key, [{ ...file, repoPath: key }]);
10981
+ }
10982
+ }
10983
+ const allPaths = [...grouped.keys()];
10984
+ const groups = [];
10985
+ for (const [repoPath, groupFiles] of grouped) {
10986
+ const stats = await Promise.all(
10987
+ groupFiles.map((f) => fs8.promises.stat(f.absolutePath).catch(() => null))
10988
+ );
10989
+ let lastModified = /* @__PURE__ */ new Date(0);
10990
+ for (const stat of stats) {
10991
+ if (stat && stat.mtime > lastModified) lastModified = stat.mtime;
10992
+ }
10993
+ const sourceNames = [...new Set(groupFiles.map((f) => f.sourceName))];
10994
+ groups.push({
10995
+ repoPath,
10996
+ label: computeLabel(repoPath, allPaths),
10997
+ files: groupFiles,
10998
+ sourceNames,
10999
+ lastModified
11000
+ });
11001
+ }
11002
+ groups.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
11003
+ return groups.filter((g) => g.files.length > 0);
11004
+ }
11005
+ async function preloadFiles(group) {
11006
+ const files = await Promise.all(
11007
+ group.files.map(async (file) => {
11008
+ if (file.content) return file;
11009
+ try {
11010
+ const buf = await fs8.promises.readFile(file.absolutePath);
11011
+ const checkLen = Math.min(buf.length, 8192);
11012
+ for (let i = 0; i < checkLen; i++) {
11013
+ if (buf[i] === 0) {
11014
+ return { ...file, content: buf, isBinary: true };
11015
+ }
11016
+ }
11017
+ return { ...file, content: buf };
11018
+ } catch {
11019
+ return file;
11020
+ }
11021
+ })
11022
+ );
11023
+ return { ...group, files };
11024
+ }
11025
+ async function runPipeline(group, middleware2, output, options, onProgress) {
11026
+ const preloaded = await preloadFiles(group);
11027
+ let processed = preloaded;
11028
+ for (const mw of middleware2) {
11029
+ onProgress?.(`${mw.name} (${processed.files.length} files)`);
11030
+ processed = await mw.process(processed);
11031
+ }
11032
+ onProgress?.(`Compressing ${processed.files.length} files`);
11033
+ return output.emit(processed, options);
11034
+ }
11035
+
11036
+ // src/debug-logs.ts
11037
+ var DEBUG_LOGS_SLUG = "debug-logs";
11038
+ var CURRENT_SCHEMA_VERSION = 1;
11039
+ var DEFAULT_WAIT_MS = 6e4;
11040
+ var LOCK_RETRIES = 100;
11041
+ var LOCK_RETRY_DELAY_MS = 100;
11042
+ function stateDir() {
11043
+ return process.env.HILLCLIMB_DEBUG_LOG_STATE_DIR ?? path11.join(configDir(), "debug-log-uploads");
11044
+ }
11045
+ function waitMs() {
11046
+ const raw = process.env.HILLCLIMB_DEBUG_LOG_WAIT_MS;
11047
+ if (!raw) return DEFAULT_WAIT_MS;
11048
+ const parsed = Number(raw);
11049
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_WAIT_MS;
11050
+ }
11051
+ function stateFile(eventId) {
11052
+ return path11.join(stateDir(), `${eventId}.json`);
11053
+ }
11054
+ function lockFile(eventId) {
11055
+ return `${stateFile(eventId)}.lock`;
11056
+ }
11057
+ function sleep2(ms) {
11058
+ return new Promise((resolve) => setTimeout(resolve, ms));
11059
+ }
11060
+ function sanitize(value) {
11061
+ return value.replace(/[^a-zA-Z0-9._-]/g, "_");
11062
+ }
11063
+ function formatEpochSeconds(date) {
11064
+ return String(Math.floor(date.getTime() / 1e3));
11065
+ }
11066
+ function toolLabel(tool) {
11067
+ const labels = {
11068
+ cursor: "Cursor",
11069
+ codex: "Codex",
11070
+ claude: "Claude",
11071
+ "copilot-chat": "GitHub Copilot Chat",
11072
+ opencode: "opencode"
11073
+ };
11074
+ return labels[tool] ?? tool;
11075
+ }
11076
+ function classifyHookEvent(event) {
11077
+ switch (event) {
11078
+ case "Stop":
11079
+ case "stop":
11080
+ case "session.idle":
11081
+ return "stop";
11082
+ case "SessionEnd":
11083
+ case "sessionEnd":
11084
+ case "session.deleted":
11085
+ case "server.instance.disposed":
11086
+ return "sessionEnd";
11087
+ default:
11088
+ return null;
11089
+ }
11090
+ }
11091
+ function resolveCwd(payload) {
11092
+ return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
11093
+ }
11094
+ function resolveSessionId(payload) {
11095
+ return payload.session_id ?? payload.conversation_id ?? null;
11096
+ }
11097
+ function stringOrNull(value) {
11098
+ return typeof value === "string" && value.length > 0 ? value : null;
11099
+ }
11100
+ function expectedKinds(tool, eventKind, payload) {
11101
+ if (eventKind === "stop") {
11102
+ return new Set(
11103
+ tool === "codex" || tool === "copilot-chat" ? ["agent", "git"] : ["git"]
11104
+ );
11105
+ }
11106
+ if (tool === "opencode" && !resolveSessionId(payload)) {
11107
+ return /* @__PURE__ */ new Set(["git"]);
11108
+ }
11109
+ return /* @__PURE__ */ new Set(["agent", "git"]);
11110
+ }
11111
+ async function transcriptFingerprint(payload) {
11112
+ const transcriptPath = stringOrNull(payload.transcript_path);
11113
+ if (!transcriptPath) return {};
11114
+ const resolved = path11.resolve(transcriptPath);
11115
+ try {
11116
+ const stat = await fs9.promises.stat(resolved);
11117
+ return {
11118
+ transcriptPath: resolved,
11119
+ transcriptMtimeMs: stat.mtimeMs,
11120
+ transcriptSizeBytes: stat.size
11121
+ };
11122
+ } catch {
11123
+ return { transcriptPath: resolved };
11124
+ }
11125
+ }
11126
+ async function eventContext(tool, payload) {
11127
+ const eventKind = classifyHookEvent(payload.hook_event_name);
11128
+ if (!eventKind) return null;
11129
+ const cwd = resolveCwd(payload);
11130
+ if (!cwd) return null;
11131
+ const project = await findProjectForCwd(cwd);
11132
+ if (!project) return null;
11133
+ const sessionId = resolveSessionId(payload);
11134
+ const turnId = stringOrNull(payload.turn_id);
11135
+ const transcriptPath = stringOrNull(payload.transcript_path);
11136
+ if (!sessionId && !turnId && !transcriptPath) {
11137
+ return null;
11138
+ }
11139
+ const expected = expectedKinds(tool, eventKind, payload);
11140
+ const eventNonce = eventKind === "stop" && expected.size === 1 && expected.has("git") && !turnId && !transcriptPath ? crypto.randomBytes(8).toString("hex") : null;
11141
+ const fingerprint = {
11142
+ schema: "debug-log-event-v1",
11143
+ apiBaseUrl: project.config.apiBaseUrl,
11144
+ projectId: project.config.projectId,
11145
+ repoRoot: project.repoRoot,
11146
+ tool,
11147
+ eventKind,
11148
+ hookEventName: payload.hook_event_name ?? null,
11149
+ sessionId,
11150
+ conversationId: stringOrNull(payload.conversation_id),
11151
+ turnId,
11152
+ eventNonce,
11153
+ ...await transcriptFingerprint(payload)
11154
+ };
11155
+ const eventId = crypto.createHash("sha256").update(JSON.stringify(fingerprint)).digest("hex").slice(0, 32);
11156
+ return {
11157
+ eventId,
11158
+ repoRoot: project.repoRoot,
11159
+ config: project.config,
11160
+ tool,
11161
+ eventKind,
11162
+ hookEventName: payload.hook_event_name ?? null,
11163
+ sessionId,
11164
+ expectedKinds: expected
11165
+ };
11166
+ }
11167
+ async function readState(eventId) {
11168
+ try {
11169
+ const raw = await fs9.promises.readFile(stateFile(eventId), "utf-8");
11170
+ const parsed = JSON.parse(raw);
11171
+ if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) return null;
11172
+ return parsed;
11173
+ } catch {
11174
+ return null;
11175
+ }
11176
+ }
11177
+ async function writeState(state) {
11178
+ await fs9.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
11179
+ const file = stateFile(state.eventId);
11180
+ const tmp = `${file}.tmp`;
11181
+ await fs9.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
11182
+ mode: 384
11183
+ });
11184
+ await fs9.promises.rename(tmp, file);
11185
+ }
11186
+ async function acquireLock(eventId) {
11187
+ await fs9.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
11188
+ for (let i = 0; i < LOCK_RETRIES; i++) {
11189
+ try {
11190
+ const fd = await fs9.promises.open(
11191
+ lockFile(eventId),
11192
+ fs9.constants.O_CREAT | fs9.constants.O_EXCL | fs9.constants.O_WRONLY
11193
+ );
11194
+ await fd.write(String(process.pid));
11195
+ await fd.close();
11196
+ return;
11197
+ } catch (err) {
11198
+ if (err.code === "EEXIST" && i < LOCK_RETRIES - 1) {
11199
+ await sleep2(LOCK_RETRY_DELAY_MS);
11200
+ continue;
11201
+ }
11202
+ throw err;
11203
+ }
11204
+ }
11205
+ }
11206
+ async function releaseLock(eventId) {
11207
+ try {
11208
+ await fs9.promises.unlink(lockFile(eventId));
11209
+ } catch {
11210
+ }
11211
+ }
11212
+ function initialState(ctx, now) {
11213
+ return {
11214
+ schemaVersion: CURRENT_SCHEMA_VERSION,
11215
+ eventId: ctx.eventId,
11216
+ apiBaseUrl: ctx.config.apiBaseUrl,
11217
+ projectId: ctx.config.projectId,
11218
+ projectSlug: ctx.config.projectSlug,
11219
+ repoRoot: ctx.repoRoot,
11220
+ tool: ctx.tool,
11221
+ eventKind: ctx.eventKind,
11222
+ hookEventName: ctx.hookEventName,
11223
+ sessionId: ctx.sessionId,
11224
+ logDate: path11.basename(todayLogPath(), ".log"),
11225
+ firstSeenAt: now.toISOString()
11226
+ };
11227
+ }
11228
+ function markDone(state, kind, now) {
11229
+ if (kind === "agent") {
11230
+ return { ...state, agentDoneAt: state.agentDoneAt ?? now.toISOString() };
11231
+ }
11232
+ return { ...state, gitDoneAt: state.gitDoneAt ?? now.toISOString() };
11233
+ }
11234
+ function expectedComplete(state, expected) {
11235
+ if (expected.has("agent") && !state.agentDoneAt) return false;
11236
+ if (expected.has("git") && !state.gitDoneAt) return false;
11237
+ return true;
11238
+ }
11239
+ function formatKinds(kinds) {
11240
+ return [...kinds].sort().join("+") || "<none>";
11241
+ }
11242
+ function observedKinds(state) {
11243
+ const observed = [];
11244
+ if (state.agentDoneAt) observed.push("agent");
11245
+ if (state.gitDoneAt) observed.push("git");
11246
+ return observed;
11247
+ }
11248
+ function elapsedSince(iso, now) {
11249
+ const started = Date.parse(iso);
11250
+ return Number.isNaN(started) ? 0 : Math.max(0, now.getTime() - started);
11251
+ }
11252
+ async function recordMarker(ctx, kind) {
11253
+ await acquireLock(ctx.eventId);
11254
+ try {
11255
+ const now = /* @__PURE__ */ new Date();
11256
+ const state = await readState(ctx.eventId) ?? initialState(ctx, now);
11257
+ const next = markDone(state, kind, now);
11258
+ await writeState(next);
11259
+ return next;
11260
+ } finally {
11261
+ await releaseLock(ctx.eventId);
11262
+ }
11263
+ }
11264
+ async function waitForExpectedKinds(ctx, state) {
11265
+ if (expectedComplete(state, ctx.expectedKinds) || state.uploadedAt) {
11266
+ return state;
11267
+ }
11268
+ const started = Date.now();
11269
+ const maxWaitMs = waitMs();
11270
+ while (Date.now() - started < maxWaitMs) {
11271
+ await sleep2(Math.min(250, Math.max(25, maxWaitMs)));
11272
+ const latest = await readState(ctx.eventId);
11273
+ if (!latest) continue;
11274
+ if (expectedComplete(latest, ctx.expectedKinds) || latest.uploadedAt) {
11275
+ return latest;
11276
+ }
11277
+ }
11278
+ appendLog(
11279
+ "warn",
11280
+ `debug-logs: timed out waiting for ${[...ctx.expectedKinds].join("+")} completion (event=${ctx.eventId}, tool=${ctx.tool})`
11281
+ );
11282
+ return await readState(ctx.eventId) ?? state;
11283
+ }
11284
+ async function buildMiddleware(repoRoot) {
11285
+ const envFileNames = await discoverEnvFiles(repoRoot);
11286
+ const envFilePaths = envFileNames.map((n) => path11.join(repoRoot, n));
11287
+ const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
11288
+ const middleware2 = [];
11289
+ if (secretResult.values.size > 0) {
11290
+ middleware2.push(new RedactMiddleware(secretResult.values));
11291
+ }
11292
+ middleware2.push(new PatternRedactMiddleware());
11293
+ return middleware2;
11294
+ }
11295
+ async function uploadDebugLog(ctx, state) {
11296
+ const logPath = todayLogPath();
11297
+ let content;
11298
+ try {
11299
+ content = await fs9.promises.readFile(logPath);
11300
+ } catch (err) {
11301
+ appendLog(
11302
+ "warn",
11303
+ `debug-logs: skipped upload; log file not readable (${logPath}): ${err instanceof Error ? err.message : String(err)}`
11304
+ );
11305
+ return null;
11306
+ }
11307
+ if (content.byteLength === 0) {
11308
+ appendLog("info", "debug-logs: skipped upload; log file is empty");
11309
+ return null;
11310
+ }
11311
+ const identity = await loadIdentity(ctx.config.apiBaseUrl);
11312
+ if (!identity) {
11313
+ appendLog(
11314
+ "warn",
11315
+ `debug-logs: skipped upload; no saved login for ${ctx.config.apiBaseUrl}`
11316
+ );
11317
+ return null;
11318
+ }
11319
+ const now = /* @__PURE__ */ new Date();
11320
+ appendLog(
11321
+ "info",
11322
+ `debug-logs: uploading event=${ctx.eventId} session=${ctx.sessionId ?? "<none>"} tool=${ctx.tool} expected=${formatKinds(ctx.expectedKinds)} observed=${formatKinds(observedKinds(state))} waitedMs=${elapsedSince(state.firstSeenAt, now)} project=${ctx.config.projectSlug} (${ctx.config.projectId}) apiBaseUrl=${ctx.config.apiBaseUrl}`
11323
+ );
11324
+ const sourceFile = {
11325
+ sourceName: DEBUG_LOGS_SLUG,
11326
+ absolutePath: logPath,
11327
+ repoPath: ctx.repoRoot,
11328
+ content
11329
+ };
11330
+ const group = {
11331
+ repoPath: ctx.repoRoot,
11332
+ label: path11.basename(ctx.repoRoot),
11333
+ files: [sourceFile],
11334
+ sourceNames: [DEBUG_LOGS_SLUG],
11335
+ lastModified: now
11336
+ };
11337
+ const shortSession = (ctx.sessionId ?? ctx.eventId).slice(0, 12);
11338
+ const epochSeconds = formatEpochSeconds(now);
11339
+ const label = toolLabel(ctx.tool);
11340
+ const client = new PlatformClient(
11341
+ ctx.config.apiBaseUrl,
11342
+ identity.sessionCookie
11343
+ );
11344
+ const output = new PlatformUploadOutput({
11345
+ client,
11346
+ projectId: ctx.config.projectId,
11347
+ contributionTypeSlug: DEBUG_LOGS_SLUG,
11348
+ contributionTitle: `${label} debug log ${shortSession} - ${epochSeconds}`,
11349
+ contributionBody: [
11350
+ `Session ID: ${ctx.sessionId ?? "<none>"}`,
11351
+ `Tool: ${label}`,
11352
+ `Event: ${ctx.hookEventName ?? ctx.eventKind}`,
11353
+ `Repo: ${ctx.repoRoot}`,
11354
+ `Log: ${path11.basename(logPath)}`,
11355
+ `Agent done: ${state.agentDoneAt ?? "<not observed>"}`,
11356
+ `Git done: ${state.gitDoneAt ?? "<not observed>"}`,
11357
+ `Uploaded: ${now.toISOString()}`
11358
+ ].join("\n"),
11359
+ zipFilename: `hillclimb-debug-log-${sanitize(ctx.tool)}-${sanitize(shortSession)}-${epochSeconds}.zip`,
11360
+ autoSubmit: true
11361
+ });
11362
+ try {
11363
+ const contributionId = await runPipeline(
11364
+ group,
11365
+ await buildMiddleware(ctx.repoRoot),
11366
+ output,
11367
+ { selectedSources: [DEBUG_LOGS_SLUG] }
11368
+ );
11369
+ appendLog(
11370
+ "info",
11371
+ `debug-logs: uploaded ${path11.basename(logPath)} to project ${ctx.config.projectSlug} (${ctx.config.projectId}) as contribution ${contributionId}`
11372
+ );
11373
+ return contributionId;
11374
+ } catch (err) {
11375
+ if (err instanceof PlatformError && err.status === 404) {
11376
+ appendLog(
11377
+ "warn",
11378
+ "debug-logs: upload skipped; platform does not have the debug-logs contribution type yet"
11379
+ );
11380
+ return null;
11381
+ }
11382
+ appendLog(
11383
+ "error",
11384
+ `debug-logs: upload failed: ${err instanceof Error ? err.message : String(err)}`
11385
+ );
11386
+ return null;
11387
+ }
11388
+ }
11389
+ async function uploadOnce(ctx, state) {
11390
+ await acquireLock(ctx.eventId);
11391
+ try {
11392
+ const latest = await readState(ctx.eventId) ?? state;
11393
+ if (latest.uploadedAt) return;
11394
+ const contributionId = await uploadDebugLog(ctx, latest);
11395
+ if (!contributionId) return;
11396
+ const uploadedAt = (/* @__PURE__ */ new Date()).toISOString();
11397
+ await writeState({
11398
+ ...latest,
11399
+ uploadedAt,
11400
+ uploadContributionId: contributionId ?? void 0
11401
+ });
11402
+ } finally {
11403
+ await releaseLock(ctx.eventId);
11404
+ }
11405
+ }
11406
+ async function recordDebugLogCompletion(args) {
11407
+ if (process.env.HILLCLIMB_DEBUG_LOG_UPLOAD === "0") return;
11408
+ try {
11409
+ const ctx = await eventContext(args.tool, args.payload);
11410
+ if (!ctx) return;
11411
+ const marked = await recordMarker(ctx, args.kind);
11412
+ const ready = await waitForExpectedKinds(ctx, marked);
11413
+ if (ready.uploadedAt) return;
11414
+ await uploadOnce(ctx, ready);
11415
+ } catch (err) {
11416
+ appendLog(
11417
+ "warn",
11418
+ `debug-logs: failed to record completion: ${err instanceof Error ? err.message : String(err)}`
11419
+ );
11420
+ }
11421
+ }
11422
+
11423
+ // src/normalizer/index.ts
11424
+ import path12 from "path";
11425
+
11426
+ // src/normalizer/claude.ts
11427
+ function stringify(value) {
11428
+ if (typeof value === "string") return value;
11429
+ try {
11430
+ return JSON.stringify(value);
11431
+ } catch {
11432
+ return String(value);
11433
+ }
11434
+ }
11435
+ function extractTextReasoningToolUses(content) {
11436
+ if (typeof content === "string") {
11437
+ return [content.trim(), void 0, []];
11438
+ }
11439
+ const textParts = [];
11440
+ const reasoningParts = [];
11441
+ const toolBlocks = [];
11442
+ if (Array.isArray(content)) {
11443
+ for (const block of content) {
11444
+ if (typeof block !== "object" || block === null) {
11445
+ textParts.push(stringify(block));
11446
+ continue;
11447
+ }
11448
+ const b = block;
11449
+ const blockType = b.type;
11450
+ if (blockType === "tool_use") {
11451
+ toolBlocks.push(b);
11452
+ continue;
11453
+ }
11454
+ if (blockType === "thinking" || blockType === "reasoning" || blockType === "analysis") {
11455
+ const textValue2 = b.text !== void 0 && b.text !== null ? b.text : b.thinking;
11456
+ if (typeof textValue2 === "string") {
11457
+ reasoningParts.push(textValue2.trim());
11458
+ } else {
11459
+ reasoningParts.push(stringify(textValue2));
11460
+ }
11461
+ continue;
11462
+ }
11463
+ if (blockType === "code" && typeof b.code === "string") {
11464
+ textParts.push(b.code);
11465
+ continue;
11466
+ }
11467
+ const textValue = b.text;
11468
+ if (typeof textValue === "string") {
11469
+ textParts.push(textValue);
11470
+ } else {
11471
+ textParts.push(stringify(b));
11472
+ }
11473
+ }
11474
+ } else if (content !== void 0 && content !== null) {
11475
+ textParts.push(stringify(content));
11476
+ }
11477
+ const text3 = textParts.filter((p7) => p7 && p7.trim()).map((p7) => p7.trim()).join("\n\n");
11478
+ const reasoning = reasoningParts.filter((p7) => p7 && p7.trim()).map((p7) => p7.trim()).join("\n\n");
11479
+ return [text3, reasoning || void 0, toolBlocks];
11480
+ }
11481
+ function buildMetrics(usage) {
11482
+ if (typeof usage !== "object" || usage === null) return void 0;
11483
+ const u = usage;
11484
+ const cachedTokens = u.cache_read_input_tokens || 0;
11485
+ const creation = u.cache_creation_input_tokens || 0;
11486
+ const inputTokens = u.input_tokens || 0;
11487
+ const promptTokens = inputTokens + cachedTokens + creation;
11488
+ const completionTokens = u.output_tokens || 0;
11489
+ const extra = {};
11490
+ for (const [key, value] of Object.entries(u)) {
11491
+ if (key === "input_tokens" || key === "output_tokens") continue;
11492
+ extra[key] = value;
11493
+ }
11494
+ return {
11495
+ prompt_tokens: promptTokens,
11496
+ completion_tokens: completionTokens,
11497
+ cached_tokens: cachedTokens,
11498
+ extra: Object.keys(extra).length > 0 ? extra : void 0
11499
+ };
11500
+ }
11501
+ function formatToolResult(block, toolUseResult) {
11502
+ const parts = [];
11503
+ const content = block.content;
11504
+ if (typeof content === "string") {
11505
+ if (content.trim()) parts.push(content.trim());
11506
+ } else if (Array.isArray(content)) {
11507
+ for (const item of content) {
11508
+ const text3 = stringify(item);
11509
+ if (text3.trim()) parts.push(text3.trim());
11510
+ }
11511
+ } else if (content !== void 0 && content !== null && content !== "") {
11512
+ parts.push(stringify(content));
11513
+ }
11514
+ let metadata;
11515
+ if (toolUseResult && typeof toolUseResult === "object") {
11516
+ metadata = { tool_use_result: toolUseResult };
11517
+ const stdout = toolUseResult.stdout;
11518
+ const stderr = toolUseResult.stderr;
11519
+ const exitCode = toolUseResult.exitCode ?? toolUseResult.exit_code;
11520
+ const interrupted = toolUseResult.interrupted;
11521
+ const isImage = toolUseResult.isImage;
11522
+ const formatted = [];
11523
+ if (stdout) formatted.push(`[stdout]
11524
+ ${stdout}`.trimEnd());
11525
+ if (stderr) formatted.push(`[stderr]
11526
+ ${stderr}`.trimEnd());
11527
+ if (exitCode !== void 0 && exitCode !== null && exitCode !== 0)
11528
+ formatted.push(`[exit_code] ${exitCode}`);
11529
+ if (interrupted) formatted.push(`[interrupted] ${interrupted}`);
11530
+ if (isImage) formatted.push(`[is_image] ${isImage}`);
11531
+ const skipKeys = /* @__PURE__ */ new Set([
11532
+ "stdout",
11533
+ "stderr",
11534
+ "exitCode",
11535
+ "exit_code",
11536
+ "interrupted",
11537
+ "isImage"
11538
+ ]);
11539
+ const remainingMeta = {};
11540
+ for (const [k, v] of Object.entries(toolUseResult)) {
11541
+ if (!skipKeys.has(k)) remainingMeta[k] = v;
11542
+ }
11543
+ if (Object.keys(remainingMeta).length > 0) {
11544
+ formatted.push(`[metadata] ${JSON.stringify(remainingMeta)}`);
11545
+ }
11546
+ if (formatted.length > 0) {
11547
+ parts.push(formatted.filter(Boolean).join("\n"));
11548
+ }
11549
+ }
11550
+ if (block.is_error === true) {
11551
+ parts.push("[error] tool reported failure");
11552
+ metadata = metadata || {};
11553
+ metadata.is_error = true;
11554
+ }
11555
+ if (metadata !== void 0) {
11556
+ if (!("raw_tool_result" in metadata)) {
11557
+ metadata.raw_tool_result = block;
11558
+ }
11559
+ }
11560
+ const resultText = parts.filter(Boolean).join("\n\n").trim();
11561
+ return [resultText || void 0, metadata];
10923
11562
  }
10924
11563
  function convertClaudeToTrajectory(jsonlContent, sessionId) {
10925
11564
  const rawEvents = [];
@@ -11304,6 +11943,59 @@ function parseOutputBlob(raw) {
11304
11943
  }
11305
11944
  return [String(parsed), void 0];
11306
11945
  }
11946
+ function asObject(value) {
11947
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
11948
+ }
11949
+ function compactExtra(extra) {
11950
+ const compacted = {};
11951
+ for (const [key, value] of Object.entries(extra)) {
11952
+ if (value !== void 0 && value !== null) compacted[key] = value;
11953
+ }
11954
+ return Object.keys(compacted).length > 0 ? compacted : void 0;
11955
+ }
11956
+ function parseJsonObject(raw) {
11957
+ const obj = asObject(raw);
11958
+ if (obj) return obj;
11959
+ if (typeof raw !== "string") return void 0;
11960
+ try {
11961
+ return asObject(JSON.parse(raw));
11962
+ } catch {
11963
+ return void 0;
11964
+ }
11965
+ }
11966
+ function subagentRefFromSpawnOutput(args, rawOutput) {
11967
+ const output = parseJsonObject(rawOutput);
11968
+ const sessionId = typeof output?.agent_id === "string" && output.agent_id || typeof output?.agent_path === "string" && output.agent_path || void 0;
11969
+ if (!sessionId) return void 0;
11970
+ const extra = compactExtra({
11971
+ agent_role: typeof args?.agent_type === "string" && args.agent_type || typeof output?.agent_role === "string" && output.agent_role || void 0,
11972
+ nickname: typeof output?.nickname === "string" && output.nickname || typeof output?.agent_nickname === "string" && output.agent_nickname || void 0
11973
+ });
11974
+ return {
11975
+ session_id: sessionId,
11976
+ extra
11977
+ };
11978
+ }
11979
+ function codexTrajectoryExtra(metaPayload) {
11980
+ const source = asObject(metaPayload.source);
11981
+ const subagent = asObject(source?.subagent);
11982
+ const threadSpawn = asObject(subagent?.thread_spawn);
11983
+ const parentThreadId = typeof threadSpawn?.parent_thread_id === "string" ? threadSpawn.parent_thread_id : void 0;
11984
+ const subagentExtra = threadSpawn ? compactExtra({
11985
+ depth: threadSpawn.depth,
11986
+ agent_path: threadSpawn.agent_path,
11987
+ agent_nickname: threadSpawn.agent_nickname,
11988
+ agent_role: threadSpawn.agent_role
11989
+ }) : void 0;
11990
+ return compactExtra({
11991
+ thread_source: metaPayload.thread_source,
11992
+ agent_nickname: metaPayload.agent_nickname,
11993
+ agent_role: metaPayload.agent_role,
11994
+ parent_session_id: parentThreadId,
11995
+ parent_thread_id: parentThreadId,
11996
+ subagent: subagentExtra
11997
+ });
11998
+ }
11307
11999
  function convertCodexToTrajectory(jsonlContent, sessionId) {
11308
12000
  const rawEvents = [];
11309
12001
  for (const line of jsonlContent.split("\n")) {
@@ -11317,7 +12009,7 @@ function convertCodexToTrajectory(jsonlContent, sessionId) {
11317
12009
  if (rawEvents.length === 0) return null;
11318
12010
  const sessionMeta = rawEvents.find((e) => e.type === "session_meta");
11319
12011
  const metaPayload = sessionMeta?.payload ?? {};
11320
- const sid = sessionId ?? metaPayload.id ?? "";
12012
+ const sid = sessionId ?? (typeof metaPayload.id === "string" ? metaPayload.id : "");
11321
12013
  const agentVersion = metaPayload.cli_version ?? "unknown";
11322
12014
  const agentExtra = {};
11323
12015
  for (const key of ["originator", "cwd", "git", "instructions"]) {
@@ -11434,6 +12126,13 @@ function convertCodexToTrajectory(jsonlContent, sessionId) {
11434
12126
  callInfo.output = outputText;
11435
12127
  callInfo.metadata = metadata;
11436
12128
  callInfo.timestamp = callInfo.timestamp ?? timestamp;
12129
+ if (callInfo.tool_name === "spawn_agent") {
12130
+ const subagentRef = subagentRefFromSpawnOutput(
12131
+ callInfo.arguments,
12132
+ payload.output
12133
+ );
12134
+ if (subagentRef) callInfo.subagentRefs = [subagentRef];
12135
+ }
11437
12136
  normalizedEvents.push(callInfo);
11438
12137
  pendingReasoning = void 0;
11439
12138
  }
@@ -11489,7 +12188,8 @@ function convertCodexToTrajectory(jsonlContent, sessionId) {
11489
12188
  extra: Object.keys(agentExtra).length > 0 ? agentExtra : void 0
11490
12189
  },
11491
12190
  steps,
11492
- final_metrics: finalMetrics
12191
+ final_metrics: finalMetrics,
12192
+ extra: codexTrajectoryExtra(metaPayload)
11493
12193
  };
11494
12194
  }
11495
12195
  function convertEventToStep2(event, stepId, defaultModelName) {
@@ -11524,7 +12224,8 @@ function convertEventToStep2(event, stepId, defaultModelName) {
11524
12224
  if (event.output !== void 0) {
11525
12225
  const result = {
11526
12226
  source_call_id: callId || void 0,
11527
- content: event.output
12227
+ content: event.output,
12228
+ subagent_trajectory_ref: event.subagentRefs
11528
12229
  };
11529
12230
  observation = { results: [result] };
11530
12231
  }
@@ -11572,13 +12273,13 @@ function excludeNone(obj) {
11572
12273
  }
11573
12274
 
11574
12275
  // src/normalizer/copilotChat.ts
11575
- function asObject(value) {
12276
+ function asObject2(value) {
11576
12277
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
11577
12278
  }
11578
12279
  function asNumber(value) {
11579
12280
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
11580
12281
  }
11581
- function compactExtra(extra) {
12282
+ function compactExtra2(extra) {
11582
12283
  const result = {};
11583
12284
  for (const [key, value] of Object.entries(extra)) {
11584
12285
  if (value !== void 0 && value !== null) result[key] = value;
@@ -11591,11 +12292,11 @@ function parseJsonLines(content) {
11591
12292
  const trimmed = line.trim();
11592
12293
  if (!trimmed) continue;
11593
12294
  try {
11594
- const parsed = asObject(JSON.parse(trimmed));
12295
+ const parsed = asObject2(JSON.parse(trimmed));
11595
12296
  if (!parsed) continue;
11596
12297
  entries.push({
11597
12298
  type: typeof parsed.type === "string" ? parsed.type : void 0,
11598
- data: asObject(parsed.data),
12299
+ data: asObject2(parsed.data),
11599
12300
  id: typeof parsed.id === "string" ? parsed.id : void 0,
11600
12301
  timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : void 0
11601
12302
  });
@@ -11605,11 +12306,11 @@ function parseJsonLines(content) {
11605
12306
  return entries;
11606
12307
  }
11607
12308
  function parseArguments(value) {
11608
- const obj = asObject(value);
12309
+ const obj = asObject2(value);
11609
12310
  if (obj) return obj;
11610
12311
  if (typeof value === "string") {
11611
12312
  try {
11612
- const parsed = asObject(JSON.parse(value));
12313
+ const parsed = asObject2(JSON.parse(value));
11613
12314
  if (parsed) return parsed;
11614
12315
  } catch {
11615
12316
  return value ? { input: value } : {};
@@ -11631,7 +12332,7 @@ function buildMetrics2(data) {
11631
12332
  completion_tokens: outputTokens || void 0,
11632
12333
  cached_tokens: cacheReadTokens || void 0,
11633
12334
  cost_usd: cost || void 0,
11634
- extra: compactExtra({
12335
+ extra: compactExtra2({
11635
12336
  cache_write_tokens: cacheWriteTokens || void 0,
11636
12337
  duration_ms: data.duration,
11637
12338
  initiator: data.initiator,
@@ -11663,7 +12364,7 @@ function finalMetricsFromSteps(steps) {
11663
12364
  };
11664
12365
  }
11665
12366
  function makeToolCall(request) {
11666
- const req = asObject(request);
12367
+ const req = asObject2(request);
11667
12368
  if (!req) return null;
11668
12369
  const callId = typeof req.toolCallId === "string" && req.toolCallId || typeof req.id === "string" && req.id || "";
11669
12370
  const name = typeof req.name === "string" && req.name || typeof req.toolName === "string" && req.toolName || "tool";
@@ -11683,7 +12384,7 @@ function toolCallFromExecutionStart(data) {
11683
12384
  };
11684
12385
  }
11685
12386
  function contentFromToolResult(data) {
11686
- const result = asObject(data.result);
12387
+ const result = asObject2(data.result);
11687
12388
  if (typeof result?.content === "string") return result.content;
11688
12389
  if (typeof data.content === "string") return data.content;
11689
12390
  return void 0;
@@ -11709,7 +12410,7 @@ function convertCopilotChatToTrajectory(jsonlContent, sessionId) {
11709
12410
  copilotVersion = data.copilotVersion;
11710
12411
  if (typeof data.vscodeVersion === "string")
11711
12412
  vscodeVersion = data.vscodeVersion;
11712
- const context = asObject(data.context);
12413
+ const context = asObject2(data.context);
11713
12414
  if (typeof context?.cwd === "string") cwd = context.cwd;
11714
12415
  continue;
11715
12416
  }
@@ -11722,7 +12423,7 @@ function convertCopilotChatToTrajectory(jsonlContent, sessionId) {
11722
12423
  timestamp: entry.timestamp,
11723
12424
  source,
11724
12425
  message: content,
11725
- extra: compactExtra({
12426
+ extra: compactExtra2({
11726
12427
  attachments: data.attachments,
11727
12428
  source: data.source,
11728
12429
  agent_mode: data.agentMode,
@@ -11752,7 +12453,7 @@ function convertCopilotChatToTrajectory(jsonlContent, sessionId) {
11752
12453
  source: "agent",
11753
12454
  message: content || (toolCalls.length > 0 ? "(tool use)" : ""),
11754
12455
  model_name: defaultModelName,
11755
- extra: compactExtra({
12456
+ extra: compactExtra2({
11756
12457
  message_id: data.messageId,
11757
12458
  phase: data.phase,
11758
12459
  output_tokens: data.outputTokens
@@ -11798,7 +12499,7 @@ function convertCopilotChatToTrajectory(jsonlContent, sessionId) {
11798
12499
  target.observation = observation;
11799
12500
  const extra = { ...target.extra ?? {} };
11800
12501
  extra.tool_success = data.success;
11801
- target.extra = compactExtra(extra);
12502
+ target.extra = compactExtra2(extra);
11802
12503
  }
11803
12504
  if (callId) pendingToolSteps.delete(callId);
11804
12505
  continue;
@@ -11824,7 +12525,7 @@ function convertCopilotChatToTrajectory(jsonlContent, sessionId) {
11824
12525
  name: "github-copilot-chat",
11825
12526
  version: copilotVersion,
11826
12527
  model_name: defaultModelName,
11827
- extra: compactExtra({
12528
+ extra: compactExtra2({
11828
12529
  vscode_version: vscodeVersion,
11829
12530
  cwd
11830
12531
  })
@@ -11892,7 +12593,7 @@ function convertCursorToTrajectory(jsonlContent, sessionId) {
11892
12593
  }
11893
12594
 
11894
12595
  // src/normalizer/opencode.ts
11895
- function asObject2(value) {
12596
+ function asObject3(value) {
11896
12597
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
11897
12598
  }
11898
12599
  function asArray(value) {
@@ -11901,7 +12602,7 @@ function asArray(value) {
11901
12602
  function asNumber2(value) {
11902
12603
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
11903
12604
  }
11904
- function compactExtra2(extra) {
12605
+ function compactExtra3(extra) {
11905
12606
  const result = {};
11906
12607
  for (const [key, value] of Object.entries(extra)) {
11907
12608
  if (value !== void 0 && value !== null) result[key] = value;
@@ -11915,7 +12616,7 @@ function parseJsonLines2(content) {
11915
12616
  if (!trimmed) continue;
11916
12617
  try {
11917
12618
  const parsed = JSON.parse(trimmed);
11918
- const obj = asObject2(parsed);
12619
+ const obj = asObject3(parsed);
11919
12620
  if (obj) events.push(obj);
11920
12621
  } catch {
11921
12622
  }
@@ -11930,7 +12631,7 @@ function timestampToIso(value) {
11930
12631
  return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
11931
12632
  }
11932
12633
  function timeFromObject(value) {
11933
- const obj = asObject2(value);
12634
+ const obj = asObject3(value);
11934
12635
  if (!obj) return void 0;
11935
12636
  return timestampToIso(obj.start ?? obj.created ?? obj.completed ?? obj.end);
11936
12637
  }
@@ -11943,12 +12644,12 @@ function stringify2(value) {
11943
12644
  }
11944
12645
  }
11945
12646
  function argsFromUnknown(value) {
11946
- const obj = asObject2(value);
12647
+ const obj = asObject3(value);
11947
12648
  if (obj) return obj;
11948
12649
  if (typeof value === "string") {
11949
12650
  try {
11950
12651
  const parsed = JSON.parse(value);
11951
- const parsedObj = asObject2(parsed);
12652
+ const parsedObj = asObject3(parsed);
11952
12653
  if (parsedObj) return parsedObj;
11953
12654
  } catch {
11954
12655
  return value ? { input: value } : {};
@@ -11957,16 +12658,16 @@ function argsFromUnknown(value) {
11957
12658
  return value === void 0 || value === null ? {} : { value };
11958
12659
  }
11959
12660
  function modelNameFromInfo(info) {
11960
- const model = asObject2(info.model);
12661
+ const model = asObject3(info.model);
11961
12662
  const modelID = typeof info.modelID === "string" && info.modelID || typeof model?.modelID === "string" && model.modelID || void 0;
11962
12663
  const providerID = typeof info.providerID === "string" && info.providerID || typeof model?.providerID === "string" && model.providerID || void 0;
11963
12664
  if (providerID && modelID) return `${providerID}/${modelID}`;
11964
12665
  return modelID;
11965
12666
  }
11966
12667
  function metricsFromTokens(tokens, cost) {
11967
- const t = asObject2(tokens);
12668
+ const t = asObject3(tokens);
11968
12669
  if (!t) return void 0;
11969
- const cache = asObject2(t.cache);
12670
+ const cache = asObject3(t.cache);
11970
12671
  const input = asNumber2(t.input) ?? 0;
11971
12672
  const output = asNumber2(t.output) ?? 0;
11972
12673
  const reasoning = asNumber2(t.reasoning) ?? 0;
@@ -11976,7 +12677,7 @@ function metricsFromTokens(tokens, cost) {
11976
12677
  if (!input && !output && !cacheRead && !cacheWrite && !costUsd) {
11977
12678
  return void 0;
11978
12679
  }
11979
- const extra = compactExtra2({
12680
+ const extra = compactExtra3({
11980
12681
  reasoning_tokens: reasoning || void 0,
11981
12682
  cache_write_tokens: cacheWrite || void 0
11982
12683
  });
@@ -12017,12 +12718,12 @@ function finalMetricsFromSteps2(steps) {
12017
12718
  };
12018
12719
  }
12019
12720
  function entryFromLine(line) {
12020
- const info = asObject2(line.info);
12721
+ const info = asObject3(line.info);
12021
12722
  if (info) {
12022
12723
  return {
12023
12724
  info,
12024
12725
  parts: asArray(line.parts).flatMap((part) => {
12025
- const obj = asObject2(part);
12726
+ const obj = asObject3(part);
12026
12727
  return obj ? [obj] : [];
12027
12728
  })
12028
12729
  };
@@ -12031,7 +12732,7 @@ function entryFromLine(line) {
12031
12732
  return {
12032
12733
  info: line,
12033
12734
  parts: asArray(line.parts).flatMap((part) => {
12034
- const obj = asObject2(part);
12735
+ const obj = asObject3(part);
12035
12736
  return obj ? [obj] : [];
12036
12737
  })
12037
12738
  };
@@ -12050,9 +12751,9 @@ function entriesFromEventWrappers(lines) {
12050
12751
  }
12051
12752
  for (const line of lines) {
12052
12753
  const type = line.type;
12053
- const props = asObject2(line.properties) ?? line;
12754
+ const props = asObject3(line.properties) ?? line;
12054
12755
  if (type === "message.updated") {
12055
- const info = asObject2(props.info);
12756
+ const info = asObject3(props.info);
12056
12757
  const id = typeof info?.id === "string" ? info.id : void 0;
12057
12758
  if (!info || !id) continue;
12058
12759
  const entry = getEntry(id, info.sessionID);
@@ -12060,7 +12761,7 @@ function entriesFromEventWrappers(lines) {
12060
12761
  continue;
12061
12762
  }
12062
12763
  if (type === "message.part.updated") {
12063
- const part = asObject2(props.part);
12764
+ const part = asObject3(props.part);
12064
12765
  const messageID = typeof part?.messageID === "string" && part.messageID || void 0;
12065
12766
  if (!part || !messageID) continue;
12066
12767
  const entry = getEntry(messageID, part.sessionID);
@@ -12085,8 +12786,8 @@ function getAgentVersion(exportInfo) {
12085
12786
  }
12086
12787
  function sortEntries(entries) {
12087
12788
  return [...entries].sort((a, b) => {
12088
- const at = asNumber2(asObject2(a.info.time)?.created) ?? 0;
12089
- const bt = asNumber2(asObject2(b.info.time)?.created) ?? 0;
12789
+ const at = asNumber2(asObject3(a.info.time)?.created) ?? 0;
12790
+ const bt = asNumber2(asObject3(b.info.time)?.created) ?? 0;
12090
12791
  return at - bt;
12091
12792
  });
12092
12793
  }
@@ -12143,7 +12844,7 @@ function buildUserStep(entry, stepId, defaultModelName) {
12143
12844
  source: "user",
12144
12845
  message,
12145
12846
  model_name: defaultModelName,
12146
- extra: compactExtra2(extra)
12847
+ extra: compactExtra3(extra)
12147
12848
  };
12148
12849
  }
12149
12850
  function buildAgentStep(parts, info, stepId, defaultModelName, fallbackMetrics) {
@@ -12173,7 +12874,7 @@ function buildAgentStep(parts, info, stepId, defaultModelName, fallbackMetrics)
12173
12874
  break;
12174
12875
  }
12175
12876
  case "tool": {
12176
- const state = asObject2(part.state) ?? {};
12877
+ const state = asObject3(part.state) ?? {};
12177
12878
  const callID = typeof part.callID === "string" && part.callID || typeof part.id === "string" && part.id || "";
12178
12879
  const toolName = typeof part.tool === "string" && part.tool || "tool";
12179
12880
  const input = argsFromUnknown(state.input);
@@ -12227,7 +12928,7 @@ function buildAgentStep(parts, info, stepId, defaultModelName, fallbackMetrics)
12227
12928
  if (toolCalls.length > 0) step.tool_calls = toolCalls;
12228
12929
  if (observation) step.observation = observation;
12229
12930
  if (metrics) step.metrics = metrics;
12230
- const compactedExtra = compactExtra2(extra);
12931
+ const compactedExtra = compactExtra3(extra);
12231
12932
  if (compactedExtra) step.extra = compactedExtra;
12232
12933
  return step;
12233
12934
  }
@@ -12243,7 +12944,7 @@ function buildUnavailableAgentStep(info, stepId, defaultModelName) {
12243
12944
  message: "(message unavailable)",
12244
12945
  model_name: modelNameFromInfo(info) ?? defaultModelName,
12245
12946
  metrics,
12246
- extra: compactExtra2({
12947
+ extra: compactExtra3({
12247
12948
  content_unavailable: true,
12248
12949
  finish_reason: info.finish,
12249
12950
  error: info.error
@@ -12313,14 +13014,14 @@ function convertRunEventsToTrajectory(events, sessionId) {
12313
13014
  }
12314
13015
  if (type === "step_finish") {
12315
13016
  if (current) {
12316
- current.finish = asObject2(event.part) ?? {};
13017
+ current.finish = asObject3(event.part) ?? {};
12317
13018
  turns.push(current);
12318
13019
  current = null;
12319
13020
  }
12320
13021
  continue;
12321
13022
  }
12322
13023
  if (current && (type === "text" || type === "reasoning" || type === "tool_use")) {
12323
- const part = asObject2(event.part);
13024
+ const part = asObject3(event.part);
12324
13025
  if (part) current.parts.push(part);
12325
13026
  }
12326
13027
  }
@@ -12362,17 +13063,17 @@ function convertOpenCodeToTrajectory(content, sessionId) {
12362
13063
  if (!trimmed) return null;
12363
13064
  try {
12364
13065
  const parsed = JSON.parse(trimmed);
12365
- const parsedObj = asObject2(parsed);
13066
+ const parsedObj = asObject3(parsed);
12366
13067
  const messages = asArray(parsedObj?.messages);
12367
13068
  if (parsedObj && messages.length > 0) {
12368
13069
  const entries2 = messages.flatMap((message) => {
12369
- const entry = entryFromLine(asObject2(message) ?? {});
13070
+ const entry = entryFromLine(asObject3(message) ?? {});
12370
13071
  return entry ? [entry] : [];
12371
13072
  });
12372
13073
  return convertMessageEntriesToTrajectory(
12373
13074
  entries2,
12374
13075
  sessionId,
12375
- asObject2(parsedObj.info)
13076
+ asObject3(parsedObj.info)
12376
13077
  );
12377
13078
  }
12378
13079
  } catch {
@@ -12405,228 +13106,46 @@ function normalizeContent(sourceName, content, sessionId) {
12405
13106
  return null;
12406
13107
  }
12407
13108
  }
12408
- var NormalizeMiddleware = class {
12409
- name = "normalize";
12410
- async process(group) {
12411
- const newFiles = [];
12412
- for (const file of group.files) {
12413
- newFiles.push(file);
12414
- if (!file.absolutePath.endsWith(".jsonl")) continue;
12415
- if (!["claude", "codex", "cursor", "opencode", "copilot-chat"].includes(
12416
- file.sourceName
12417
- ))
12418
- continue;
12419
- const content = file.content ? file.content.toString("utf-8") : null;
12420
- if (!content) continue;
12421
- const sessionId = file.metadata?.sessionId ?? path9.basename(file.absolutePath, ".jsonl");
12422
- try {
12423
- const trajectory = normalizeContent(
12424
- file.sourceName,
12425
- content,
12426
- sessionId
12427
- );
12428
- if (!trajectory) continue;
12429
- const json = JSON.stringify(
12430
- excludeNone(trajectory),
12431
- null,
12432
- 2
12433
- );
12434
- const atifPath = file.absolutePath.replace(/\.jsonl$/, ".atif.json");
12435
- newFiles.push({
12436
- sourceName: file.sourceName,
12437
- absolutePath: atifPath,
12438
- repoPath: file.repoPath,
12439
- metadata: { ...file.metadata, isAtif: true },
12440
- content: Buffer.from(json, "utf-8")
12441
- });
12442
- } catch {
12443
- }
12444
- }
12445
- return { ...group, files: newFiles };
12446
- }
12447
- };
12448
-
12449
- // src/outputs/platform.ts
12450
- import { PassThrough } from "stream";
12451
- import archiver from "archiver";
12452
-
12453
- // src/outputs/archive.ts
12454
- import os4 from "os";
12455
- import path10 from "path";
12456
- function getSourceBaseDir(sourceName) {
12457
- const home = os4.homedir();
12458
- switch (sourceName) {
12459
- case "claude":
12460
- return path10.join(home, ".claude", "projects");
12461
- case "codex":
12462
- return path10.join(home, ".codex", "sessions");
12463
- default:
12464
- return home;
12465
- }
12466
- }
12467
- function addGroupToArchive(archive, group, selectedSources) {
12468
- for (const file of group.files) {
12469
- if (!selectedSources.has(file.sourceName)) continue;
12470
- const baseDir = getSourceBaseDir(file.sourceName);
12471
- const relativePath = file.absolutePath.startsWith(baseDir) ? path10.relative(baseDir, file.absolutePath) : path10.basename(file.absolutePath);
12472
- const archivePath = path10.join(file.sourceName, relativePath);
12473
- if (file.content) {
12474
- archive.append(file.content, { name: archivePath });
12475
- } else {
12476
- archive.file(file.absolutePath, { name: archivePath });
12477
- }
12478
- }
12479
- }
12480
-
12481
- // src/outputs/platform.ts
12482
- async function buildZipBuffer(group, selectedSources) {
12483
- const archive = archiver("zip", { zlib: { level: 6 } });
12484
- const stream = new PassThrough();
12485
- archive.pipe(stream);
12486
- const chunks = [];
12487
- const done = new Promise((resolve, reject) => {
12488
- stream.on("data", (chunk) => chunks.push(chunk));
12489
- stream.on("end", resolve);
12490
- stream.on("error", reject);
12491
- archive.on("error", reject);
12492
- });
12493
- addGroupToArchive(archive, group, selectedSources);
12494
- await archive.finalize();
12495
- await done;
12496
- return Buffer.concat(chunks);
12497
- }
12498
- var PlatformUploadOutput = class {
12499
- constructor(opts) {
12500
- this.opts = opts;
12501
- }
12502
- name = "platform";
12503
- label = "Upload to hillclimb platform";
12504
- async emit(group, options) {
12505
- const selectedSources = new Set(options.selectedSources);
12506
- const buffer = await buildZipBuffer(group, selectedSources);
12507
- const {
12508
- client,
12509
- projectId,
12510
- contributionTypeSlug,
12511
- contributionTitle,
12512
- contributionBody,
12513
- zipFilename,
12514
- autoSubmit
12515
- } = this.opts;
12516
- const contribution = await client.createContribution(projectId, {
12517
- contributionTypeSlug,
12518
- title: contributionTitle,
12519
- body: contributionBody
12520
- });
12521
- const presigned = await client.createUpload(contribution.id, {
12522
- originalFilename: zipFilename,
12523
- mimeType: "application/zip",
12524
- sizeBytes: buffer.byteLength
12525
- });
12526
- appendLog(
12527
- "info",
12528
- `uploading ${zipFilename} (${buffer.byteLength} bytes) to presigned URL`
12529
- );
12530
- await client.uploadToPresignedUrl(
12531
- presigned.presignedUrl,
12532
- presigned.headers,
12533
- buffer
12534
- );
12535
- appendLog("info", `PUT to presigned URL succeeded for ${zipFilename}`);
12536
- if (autoSubmit) {
12537
- appendLog("info", `submitting contribution ${contribution.id}`);
12538
- await client.submitContribution(contribution.id);
12539
- appendLog("info", `contribution ${contribution.id} submitted`);
12540
- }
12541
- return contribution.id;
12542
- }
12543
- };
12544
-
12545
- // src/pipeline.ts
12546
- import fs8 from "fs";
12547
- import path11 from "path";
12548
- function canonicalizePath(p7) {
12549
- let resolved = path11.resolve(p7);
12550
- if (resolved.endsWith(path11.sep) && resolved !== path11.sep) {
12551
- resolved = resolved.slice(0, -1);
12552
- }
12553
- return resolved;
12554
- }
12555
- function computeLabel(repoPath, allPaths) {
12556
- const segments = repoPath.split(path11.sep).filter(Boolean);
12557
- for (let depth = 1; depth <= segments.length; depth++) {
12558
- const label = segments.slice(-depth).join("/");
12559
- const matches = allPaths.filter((p7) => {
12560
- const s = p7.split(path11.sep).filter(Boolean);
12561
- return s.slice(-depth).join("/") === label;
12562
- });
12563
- if (matches.length === 1) return label;
12564
- }
12565
- return repoPath;
12566
- }
12567
- async function mergeByRepo(files) {
12568
- const grouped = /* @__PURE__ */ new Map();
12569
- for (const file of files) {
12570
- const key = canonicalizePath(file.repoPath);
12571
- const existing = grouped.get(key);
12572
- if (existing) {
12573
- existing.push({ ...file, repoPath: key });
12574
- } else {
12575
- grouped.set(key, [{ ...file, repoPath: key }]);
12576
- }
12577
- }
12578
- const allPaths = [...grouped.keys()];
12579
- const groups = [];
12580
- for (const [repoPath, groupFiles] of grouped) {
12581
- const stats = await Promise.all(
12582
- groupFiles.map((f) => fs8.promises.stat(f.absolutePath).catch(() => null))
12583
- );
12584
- let lastModified = /* @__PURE__ */ new Date(0);
12585
- for (const stat of stats) {
12586
- if (stat && stat.mtime > lastModified) lastModified = stat.mtime;
12587
- }
12588
- const sourceNames = [...new Set(groupFiles.map((f) => f.sourceName))];
12589
- groups.push({
12590
- repoPath,
12591
- label: computeLabel(repoPath, allPaths),
12592
- files: groupFiles,
12593
- sourceNames,
12594
- lastModified
12595
- });
12596
- }
12597
- groups.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
12598
- return groups.filter((g) => g.files.length > 0);
12599
- }
12600
- async function preloadFiles(group) {
12601
- const files = await Promise.all(
12602
- group.files.map(async (file) => {
12603
- if (file.content) return file;
13109
+ var NormalizeMiddleware = class {
13110
+ name = "normalize";
13111
+ async process(group) {
13112
+ const newFiles = [];
13113
+ for (const file of group.files) {
13114
+ newFiles.push(file);
13115
+ if (!file.absolutePath.endsWith(".jsonl")) continue;
13116
+ if (!["claude", "codex", "cursor", "opencode", "copilot-chat"].includes(
13117
+ file.sourceName
13118
+ ))
13119
+ continue;
13120
+ const content = file.content ? file.content.toString("utf-8") : null;
13121
+ if (!content) continue;
13122
+ const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 : path12.basename(file.absolutePath, ".jsonl"));
12604
13123
  try {
12605
- const buf = await fs8.promises.readFile(file.absolutePath);
12606
- const checkLen = Math.min(buf.length, 8192);
12607
- for (let i = 0; i < checkLen; i++) {
12608
- if (buf[i] === 0) {
12609
- return { ...file, content: buf, isBinary: true };
12610
- }
12611
- }
12612
- return { ...file, content: buf };
13124
+ const trajectory = normalizeContent(
13125
+ file.sourceName,
13126
+ content,
13127
+ sessionId
13128
+ );
13129
+ if (!trajectory) continue;
13130
+ const json = JSON.stringify(
13131
+ excludeNone(trajectory),
13132
+ null,
13133
+ 2
13134
+ );
13135
+ const atifPath = file.absolutePath.replace(/\.jsonl$/, ".atif.json");
13136
+ newFiles.push({
13137
+ sourceName: file.sourceName,
13138
+ absolutePath: atifPath,
13139
+ repoPath: file.repoPath,
13140
+ metadata: { ...file.metadata, isAtif: true },
13141
+ content: Buffer.from(json, "utf-8")
13142
+ });
12613
13143
  } catch {
12614
- return file;
12615
13144
  }
12616
- })
12617
- );
12618
- return { ...group, files };
12619
- }
12620
- async function runPipeline(group, middleware2, output, options, onProgress) {
12621
- const preloaded = await preloadFiles(group);
12622
- let processed = preloaded;
12623
- for (const mw of middleware2) {
12624
- onProgress?.(`${mw.name} (${processed.files.length} files)`);
12625
- processed = await mw.process(processed);
13145
+ }
13146
+ return { ...group, files: newFiles };
12626
13147
  }
12627
- onProgress?.(`Compressing ${processed.files.length} files`);
12628
- return output.emit(processed, options);
12629
- }
13148
+ };
12630
13149
 
12631
13150
  // src/commands/upload.ts
12632
13151
  async function readStdin() {
@@ -12637,17 +13156,17 @@ async function readStdin() {
12637
13156
  }
12638
13157
  return Buffer.concat(chunks).toString("utf-8");
12639
13158
  }
12640
- function sanitize(value) {
13159
+ function sanitize2(value) {
12641
13160
  return value.replace(/[^a-zA-Z0-9._-]/g, "_");
12642
13161
  }
12643
- function formatEpochSeconds(date) {
13162
+ function formatEpochSeconds2(date) {
12644
13163
  return String(Math.floor(date.getTime() / 1e3));
12645
13164
  }
12646
13165
  function lineHasAssistant(line) {
12647
13166
  return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
12648
13167
  }
12649
13168
  async function hasAssistantMessage(transcriptPath) {
12650
- const stream = fs9.createReadStream(transcriptPath, { encoding: "utf-8" });
13169
+ const stream = fs10.createReadStream(transcriptPath, { encoding: "utf-8" });
12651
13170
  let buffer = "";
12652
13171
  try {
12653
13172
  for await (const chunk of stream) {
@@ -12675,7 +13194,25 @@ function resolveSourceTool(payload) {
12675
13194
  if (payload.hook_event_name === "Stop") return "codex";
12676
13195
  return "claude";
12677
13196
  }
13197
+ function summarizePayload(payload) {
13198
+ const sessionId = payload.session_id ?? payload.conversation_id ?? null;
13199
+ const turnId = payload.turn_id ?? null;
13200
+ const cwd = payload.cwd ?? payload.workspace_roots?.[0] ?? null;
13201
+ return JSON.stringify({
13202
+ session_id: sessionId,
13203
+ turn_id: turnId,
13204
+ cwd,
13205
+ hook_event_name: payload.hook_event_name ?? null,
13206
+ tool: payload.tool ?? null,
13207
+ model: payload.model ?? null,
13208
+ permission_mode: payload.permission_mode ?? null,
13209
+ transcript_path_present: !!payload.transcript_path,
13210
+ workspace_roots_count: payload.workspace_roots?.length ?? 0,
13211
+ cursor_version_present: !!payload.cursor_version
13212
+ });
13213
+ }
12678
13214
  async function selfHealHook(repoRoot, tool) {
13215
+ if (process.env.HILLCLIMB_SKIP_HOOK_SELF_HEAL === "1") return;
12679
13216
  try {
12680
13217
  const result = await healHookForTool(repoRoot, tool);
12681
13218
  if (result.skipped) {
@@ -12697,7 +13234,7 @@ function resolveCursorTranscriptPath(payload) {
12697
13234
  const workspace = payload.workspace_roots?.[0];
12698
13235
  if (!id || !workspace) return void 0;
12699
13236
  const encoded = workspace.replace(/^\//, "").replace(/\//g, "-");
12700
- return path12.join(
13237
+ return path13.join(
12701
13238
  os5.homedir(),
12702
13239
  ".cursor",
12703
13240
  "projects",
@@ -12733,9 +13270,9 @@ async function runUploadInner(payload) {
12733
13270
  }
12734
13271
  const { repoRoot, config } = match;
12735
13272
  await selfHealHook(repoRoot, sourceTool);
12736
- const transcriptResolved = path12.resolve(transcriptPath);
13273
+ const transcriptResolved = path13.resolve(transcriptPath);
12737
13274
  try {
12738
- const stat = await fs9.promises.stat(transcriptResolved);
13275
+ const stat = await fs10.promises.stat(transcriptResolved);
12739
13276
  if (!stat.isFile()) {
12740
13277
  appendLog(
12741
13278
  "warn",
@@ -12775,13 +13312,13 @@ async function uploadSession(args) {
12775
13312
  };
12776
13313
  const group = {
12777
13314
  repoPath: repoRoot,
12778
- label: path12.basename(repoRoot),
13315
+ label: path13.basename(repoRoot),
12779
13316
  files: [sourceFile],
12780
13317
  sourceNames: [sourceTool],
12781
13318
  lastModified: now
12782
13319
  };
12783
13320
  const envFileNames = await discoverEnvFiles(repoRoot);
12784
- const envFilePaths = envFileNames.map((n) => path12.join(repoRoot, n));
13321
+ const envFilePaths = envFileNames.map((n) => path13.join(repoRoot, n));
12785
13322
  const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
12786
13323
  const mwChain = [];
12787
13324
  if (secretResult.values.size > 0) {
@@ -12806,14 +13343,14 @@ async function uploadSession(args) {
12806
13343
  "copilot-chat": "GitHub Copilot Chat",
12807
13344
  opencode: "opencode"
12808
13345
  };
12809
- const toolLabel = toolLabels[sourceTool] ?? "Claude";
12810
- const epochSeconds = formatEpochSeconds(now);
12811
- const title = `${toolLabel} session ${shortId} \u2014 ${epochSeconds}`;
13346
+ const toolLabel2 = toolLabels[sourceTool] ?? "Claude";
13347
+ const epochSeconds = formatEpochSeconds2(now);
13348
+ const title = `${toolLabel2} session ${shortId} \u2014 ${epochSeconds}`;
12812
13349
  const body = `Session ID: ${sessionId}
12813
- Tool: ${toolLabel}
13350
+ Tool: ${toolLabel2}
12814
13351
  Repo: ${repoRoot}
12815
13352
  Uploaded: ${now.toISOString()}`;
12816
- const zipFilename = `${sourceTool}-${sanitize(shortId)}-${epochSeconds}.zip`;
13353
+ const zipFilename = `${sourceTool}-${sanitize2(shortId)}-${epochSeconds}.zip`;
12817
13354
  const output = new PlatformUploadOutput({
12818
13355
  client,
12819
13356
  projectId: config.projectId,
@@ -12929,7 +13466,6 @@ async function runUploadWorker() {
12929
13466
  appendLog("warn", "worker: invoked with empty stdin; payload expected.");
12930
13467
  return;
12931
13468
  }
12932
- appendLog("info", `worker: raw payload: ${raw.trim()}`);
12933
13469
  let payload;
12934
13470
  try {
12935
13471
  payload = JSON.parse(raw);
@@ -12942,6 +13478,7 @@ async function runUploadWorker() {
12942
13478
  }
12943
13479
  const toolOverride = process.env[TOOL_ENV_FLAG];
12944
13480
  if (toolOverride) payload.tool = toolOverride;
13481
+ appendLog("info", `worker: payload summary: ${summarizePayload(payload)}`);
12945
13482
  try {
12946
13483
  await runUploadInner(payload);
12947
13484
  } catch (err) {
@@ -12949,21 +13486,28 @@ async function runUploadWorker() {
12949
13486
  "error",
12950
13487
  `worker: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
12951
13488
  );
13489
+ } finally {
13490
+ await recordDebugLogCompletion({
13491
+ kind: "agent",
13492
+ tool: resolveSourceTool(payload),
13493
+ payload
13494
+ });
12952
13495
  }
12953
13496
  }
12954
13497
 
12955
13498
  // src/git-traces/index.ts
12956
13499
  import { spawn as spawn3 } from "child_process";
12957
- import crypto2 from "crypto";
13500
+ import crypto3 from "crypto";
12958
13501
 
12959
13502
  // src/git-traces/handlers.ts
12960
13503
  import { execFileSync as execFileSync2 } from "child_process";
13504
+ import path16 from "path";
12961
13505
 
12962
13506
  // src/git-traces/git-ops.ts
12963
13507
  import { execFileSync } from "child_process";
12964
- import fs10 from "fs";
13508
+ import fs11 from "fs";
12965
13509
  import os6 from "os";
12966
- import path13 from "path";
13510
+ import path14 from "path";
12967
13511
  import { gzipSync } from "zlib";
12968
13512
  var GIT_COMMAND_TIMEOUT_MS = 12e4;
12969
13513
  var EXEC_OPTS = {
@@ -13087,7 +13631,7 @@ function buildUntrackedTree(repoRoot) {
13087
13631
  for (const relPath of list.split("\0")) {
13088
13632
  if (!relPath) continue;
13089
13633
  try {
13090
- const stat = fs10.lstatSync(path13.join(repoRoot, relPath));
13634
+ const stat = fs11.lstatSync(path14.join(repoRoot, relPath));
13091
13635
  if (stat.size > MAX_UNTRACKED_FILE_BYTES) {
13092
13636
  appendLog(
13093
13637
  "warn",
@@ -13100,7 +13644,7 @@ function buildUntrackedTree(repoRoot) {
13100
13644
  }
13101
13645
  }
13102
13646
  if (kept.length === 0) return null;
13103
- const tmpIndex = path13.join(
13647
+ const tmpIndex = path14.join(
13104
13648
  os6.tmpdir(),
13105
13649
  `hillclimb-untracked-${Date.now()}-${process.pid}`
13106
13650
  );
@@ -13113,7 +13657,7 @@ function buildUntrackedTree(repoRoot) {
13113
13657
  return gitWithEnv(repoRoot, ["write-tree"], env);
13114
13658
  } finally {
13115
13659
  try {
13116
- fs10.unlinkSync(tmpIndex);
13660
+ fs11.unlinkSync(tmpIndex);
13117
13661
  } catch {
13118
13662
  }
13119
13663
  }
@@ -13124,7 +13668,7 @@ function buildSnapshotTree(repoRoot, stashSha) {
13124
13668
  if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA) {
13125
13669
  return trackedTree;
13126
13670
  }
13127
- const tmpIndex = path13.join(
13671
+ const tmpIndex = path14.join(
13128
13672
  os6.tmpdir(),
13129
13673
  `hillclimb-index-${Date.now()}-${process.pid}`
13130
13674
  );
@@ -13152,7 +13696,7 @@ function buildSnapshotTree(repoRoot, stashSha) {
13152
13696
  return gitWithEnv(repoRoot, ["write-tree"], env);
13153
13697
  } finally {
13154
13698
  try {
13155
- fs10.unlinkSync(tmpIndex);
13699
+ fs11.unlinkSync(tmpIndex);
13156
13700
  } catch {
13157
13701
  }
13158
13702
  }
@@ -13166,16 +13710,16 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
13166
13710
  ]);
13167
13711
  const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
13168
13712
  pinRef(repoRoot, orphanRef, orphanCommit);
13169
- const tmpFile = path13.join(
13713
+ const tmpFile = path14.join(
13170
13714
  os6.tmpdir(),
13171
13715
  `hillclimb-bundle-${Date.now()}.bundle`
13172
13716
  );
13173
13717
  try {
13174
13718
  git(repoRoot, ["bundle", "create", tmpFile, orphanRef]);
13175
- return fs10.readFileSync(tmpFile);
13719
+ return fs11.readFileSync(tmpFile);
13176
13720
  } finally {
13177
13721
  try {
13178
- fs10.unlinkSync(tmpFile);
13722
+ fs11.unlinkSync(tmpFile);
13179
13723
  } catch {
13180
13724
  }
13181
13725
  deleteRef(repoRoot, orphanRef);
@@ -13212,12 +13756,34 @@ function detectTransitionKind(repoRoot, prevHeadSha, nextHeadSha) {
13212
13756
  return "branch-switch";
13213
13757
  }
13214
13758
  }
13759
+ function parseDirtyFilesFromStatus(status) {
13760
+ const raw = Buffer.isBuffer(status) ? status.toString("utf-8") : status;
13761
+ if (!raw) return [];
13762
+ if (raw.includes("\0")) {
13763
+ const fields = raw.split("\0").filter(Boolean);
13764
+ const dirtyFiles = [];
13765
+ for (let i = 0; i < fields.length; i++) {
13766
+ const record = fields[i];
13767
+ if (record.length < 4) continue;
13768
+ const statusCode = record.slice(0, 2);
13769
+ dirtyFiles.push(record.slice(3));
13770
+ if (statusCode.includes("R") || statusCode.includes("C")) i++;
13771
+ }
13772
+ return dirtyFiles;
13773
+ }
13774
+ return raw.split("\n").filter(Boolean).map((line) => {
13775
+ const match = /^(..) (.*)$/.exec(line);
13776
+ const pathPart = match ? match[2] : line.trim();
13777
+ if (pathPart.includes(" -> ")) return pathPart.split(" -> ").at(-1) ?? "";
13778
+ return pathPart;
13779
+ }).filter(Boolean);
13780
+ }
13215
13781
  function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind, startedAt) {
13216
13782
  const headSha = safeGit(repoRoot, ["rev-parse", "HEAD"]) ?? "unknown";
13217
13783
  const branch = safeGit(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? null;
13218
13784
  const remoteUrl = safeGit(repoRoot, ["config", "--get", "remote.origin.url"]) ?? null;
13219
- const statusRaw = safeGit(repoRoot, ["status", "--porcelain"]) ?? "";
13220
- const dirtyFiles = statusRaw.split("\n").filter(Boolean).map((line) => line.slice(3));
13785
+ const statusRaw = safeGitBuffer(repoRoot, ["status", "--porcelain=v1", "-z"]) ?? Buffer.alloc(0);
13786
+ const dirtyFiles = parseDirtyFilesFromStatus(statusRaw);
13221
13787
  const authorName = safeGit(repoRoot, ["config", "user.name"]) ?? "";
13222
13788
  const authorEmail = safeGit(repoRoot, ["config", "user.email"]) ?? "";
13223
13789
  const commits = transitionKind === "commit" && prevHeadSha && headSha !== "unknown" ? enumerateCommits(repoRoot, prevHeadSha, headSha) : void 0;
@@ -13328,9 +13894,9 @@ function parseCommitFiles(repoRoot, sha) {
13328
13894
  oldPath
13329
13895
  });
13330
13896
  } else {
13331
- const path21 = parts[parts.length - 1];
13332
- indexByPath.set(path21, files.length);
13333
- files.push({ path: path21, status, additions: 0, deletions: 0 });
13897
+ const path23 = parts[parts.length - 1];
13898
+ indexByPath.set(path23, files.length);
13899
+ files.push({ path: path23, status, additions: 0, deletions: 0 });
13334
13900
  }
13335
13901
  }
13336
13902
  for (const line of numstat.split("\n")) {
@@ -13361,6 +13927,19 @@ function safeGit(repoRoot, args) {
13361
13927
  return null;
13362
13928
  }
13363
13929
  }
13930
+ function safeGitBuffer(repoRoot, args) {
13931
+ try {
13932
+ return gitBuffer(repoRoot, args);
13933
+ } catch (err) {
13934
+ if (isGitTimeoutError(err)) {
13935
+ appendLog(
13936
+ "warn",
13937
+ `git-traces: optional metadata command skipped: ${err instanceof Error ? err.message : String(err)}`
13938
+ );
13939
+ }
13940
+ return null;
13941
+ }
13942
+ }
13364
13943
  function cleanupSessionRefs(repoRoot, sessionId) {
13365
13944
  for (const prefix of [
13366
13945
  `refs/hillclimb/baseline/${sessionId}`,
@@ -13383,27 +13962,31 @@ function cleanupSessionRefs(repoRoot, sessionId) {
13383
13962
  }
13384
13963
 
13385
13964
  // src/git-traces/session-state.ts
13386
- import crypto from "crypto";
13387
- import fs11 from "fs";
13965
+ import crypto2 from "crypto";
13966
+ import fs12 from "fs";
13388
13967
  import os7 from "os";
13389
- import path14 from "path";
13390
- var CURRENT_SCHEMA_VERSION = 3;
13391
- var STATE_DIR = path14.join(os7.homedir(), ".hillclimb", "git-traces");
13392
- function stateFileForRepo(repoRoot, tool) {
13393
- const hash = crypto.createHash("sha256").update(`${path14.resolve(repoRoot)}\0${tool}`).digest("hex").slice(0, 16);
13394
- return path14.join(STATE_DIR, `${hash}.json`);
13968
+ import path15 from "path";
13969
+ var CURRENT_SCHEMA_VERSION2 = 3;
13970
+ var DEFAULT_STATE_DIR = path15.join(os7.homedir(), ".hillclimb", "git-traces");
13971
+ var LOCK_RETRIES2 = 120;
13972
+ var LOCK_RETRY_DELAY_MS2 = 500;
13973
+ function stateDir2() {
13974
+ return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR;
13975
+ }
13976
+ function stateFileForRepo(repoRoot, tool, sessionId) {
13977
+ const hash = crypto2.createHash("sha256").update(
13978
+ sessionId ? `${path15.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path15.resolve(repoRoot)}\0${tool}`
13979
+ ).digest("hex").slice(0, 16);
13980
+ return path15.join(stateDir2(), `${hash}.json`);
13395
13981
  }
13396
13982
  function lockFileForRepo(repoRoot, tool) {
13397
13983
  return `${stateFileForRepo(repoRoot, tool)}.lock`;
13398
13984
  }
13399
- async function readSessionState(repoRoot, tool) {
13985
+ async function readStateFile(file) {
13400
13986
  try {
13401
- const raw = await fs11.promises.readFile(
13402
- stateFileForRepo(repoRoot, tool),
13403
- "utf-8"
13404
- );
13987
+ const raw = await fs12.promises.readFile(file, "utf-8");
13405
13988
  const parsed = JSON.parse(raw);
13406
- if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) {
13989
+ if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION2) {
13407
13990
  return null;
13408
13991
  }
13409
13992
  return parsed;
@@ -13411,29 +13994,117 @@ async function readSessionState(repoRoot, tool) {
13411
13994
  return null;
13412
13995
  }
13413
13996
  }
13997
+ async function listScopedSessionStates(repoRoot, tool) {
13998
+ let entries;
13999
+ try {
14000
+ entries = await fs12.promises.readdir(stateDir2(), { withFileTypes: true });
14001
+ } catch {
14002
+ return [];
14003
+ }
14004
+ const states = [];
14005
+ for (const entry of entries) {
14006
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
14007
+ const file = path15.join(stateDir2(), entry.name);
14008
+ const state = await readStateFile(file);
14009
+ if (!state) continue;
14010
+ if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
14011
+ continue;
14012
+ }
14013
+ if (path15.resolve(state.repoRoot) !== path15.resolve(repoRoot)) continue;
14014
+ if (path15.resolve(file) !== path15.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
14015
+ continue;
14016
+ }
14017
+ let mtimeMs = 0;
14018
+ try {
14019
+ mtimeMs = (await fs12.promises.stat(file)).mtimeMs;
14020
+ } catch {
14021
+ continue;
14022
+ }
14023
+ states.push({ state, mtimeMs });
14024
+ }
14025
+ return states;
14026
+ }
14027
+ async function listSessionStatesForSession(tool, sessionId) {
14028
+ let entries;
14029
+ try {
14030
+ entries = await fs12.promises.readdir(stateDir2(), { withFileTypes: true });
14031
+ } catch {
14032
+ return [];
14033
+ }
14034
+ const states = [];
14035
+ for (const entry of entries) {
14036
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
14037
+ const file = path15.join(stateDir2(), entry.name);
14038
+ const state = await readStateFile(file);
14039
+ if (!state) continue;
14040
+ if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
14041
+ continue;
14042
+ }
14043
+ if (state.sessionId !== sessionId) continue;
14044
+ if (path15.resolve(file) !== path15.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
14045
+ continue;
14046
+ }
14047
+ let mtimeMs = 0;
14048
+ try {
14049
+ mtimeMs = (await fs12.promises.stat(file)).mtimeMs;
14050
+ } catch {
14051
+ continue;
14052
+ }
14053
+ states.push({ state, mtimeMs });
14054
+ }
14055
+ return states;
14056
+ }
14057
+ async function readSessionState(repoRoot, tool, sessionId) {
14058
+ if (sessionId) {
14059
+ const scoped = await readStateFile(
14060
+ stateFileForRepo(repoRoot, tool, sessionId)
14061
+ );
14062
+ if (scoped) return scoped;
14063
+ const legacy = await readStateFile(stateFileForRepo(repoRoot, tool));
14064
+ return legacy?.sessionId === sessionId ? legacy : null;
14065
+ }
14066
+ return readStateFile(stateFileForRepo(repoRoot, tool));
14067
+ }
13414
14068
  async function writeSessionState(state, tool) {
13415
- const file = stateFileForRepo(state.repoRoot, tool);
13416
- await fs11.promises.mkdir(STATE_DIR, { recursive: true, mode: 448 });
14069
+ const file = stateFileForRepo(state.repoRoot, tool, state.sessionId);
14070
+ await fs12.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
13417
14071
  const tmp = `${file}.tmp`;
13418
- await fs11.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
14072
+ await fs12.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
13419
14073
  mode: 384
13420
14074
  });
13421
- await fs11.promises.rename(tmp, file);
14075
+ await fs12.promises.rename(tmp, file);
14076
+ const legacyFile = stateFileForRepo(state.repoRoot, tool);
14077
+ const legacy = await readStateFile(legacyFile);
14078
+ if (legacy?.sessionId === state.sessionId) {
14079
+ await deleteStateFile(legacyFile);
14080
+ }
13422
14081
  }
13423
- async function deleteSessionState(repoRoot, tool) {
14082
+ async function deleteStateFile(file) {
13424
14083
  try {
13425
- await fs11.promises.unlink(stateFileForRepo(repoRoot, tool));
14084
+ await fs12.promises.unlink(file);
13426
14085
  } catch {
13427
14086
  }
13428
14087
  }
13429
- async function acquireLock(repoRoot, tool, retries = 3, delayMs = 200) {
14088
+ async function deleteSessionState(repoRoot, tool, sessionId) {
14089
+ if (sessionId) {
14090
+ await deleteStateFile(stateFileForRepo(repoRoot, tool, sessionId));
14091
+ const legacyFile = stateFileForRepo(repoRoot, tool);
14092
+ const legacy = await readStateFile(legacyFile);
14093
+ if (legacy?.sessionId === sessionId) {
14094
+ await deleteStateFile(legacyFile);
14095
+ }
14096
+ return;
14097
+ }
14098
+ await deleteStateFile(stateFileForRepo(repoRoot, tool));
14099
+ }
14100
+ async function acquireLock2(repoRoot, tool, retries = LOCK_RETRIES2, delayMs = LOCK_RETRY_DELAY_MS2) {
13430
14101
  const lockPath = lockFileForRepo(repoRoot, tool);
13431
- await fs11.promises.mkdir(STATE_DIR, { recursive: true, mode: 448 });
14102
+ await fs12.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
13432
14103
  for (let i = 0; i < retries; i++) {
13433
14104
  try {
13434
- const fd = await fs11.promises.open(
14105
+ const fd = await fs12.promises.open(
13435
14106
  lockPath,
13436
- fs11.constants.O_CREAT | fs11.constants.O_EXCL | fs11.constants.O_WRONLY
14107
+ fs12.constants.O_CREAT | fs12.constants.O_EXCL | fs12.constants.O_WRONLY
13437
14108
  );
13438
14109
  await fd.write(String(process.pid));
13439
14110
  await fd.close();
@@ -13448,9 +14119,9 @@ async function acquireLock(repoRoot, tool, retries = 3, delayMs = 200) {
13448
14119
  }
13449
14120
  throw new Error(`Failed to acquire lock after ${retries} retries`);
13450
14121
  }
13451
- async function releaseLock(repoRoot, tool) {
14122
+ async function releaseLock2(repoRoot, tool) {
13452
14123
  try {
13453
- await fs11.promises.unlink(lockFileForRepo(repoRoot, tool));
14124
+ await fs12.promises.unlink(lockFileForRepo(repoRoot, tool));
13454
14125
  } catch {
13455
14126
  }
13456
14127
  }
@@ -13459,7 +14130,8 @@ async function releaseLock(repoRoot, tool) {
13459
14130
  var CLI_VERSION = "0.2.0";
13460
14131
  var GIT_TRACES_SLUG = "git-traces";
13461
14132
  var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
13462
- function formatEpochSeconds2(date) {
14133
+ var STALE_SCOPED_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
14134
+ function formatEpochSeconds3(date) {
13463
14135
  return String(Math.floor(date.getTime() / 1e3));
13464
14136
  }
13465
14137
  var TOOL_LABELS = {
@@ -13469,10 +14141,20 @@ var TOOL_LABELS = {
13469
14141
  "copilot-chat": "GitHub Copilot Chat",
13470
14142
  opencode: "opencode"
13471
14143
  };
13472
- function resolveCwd(payload) {
14144
+ async function loadConfiguredRepos() {
14145
+ const file = await loadProjects();
14146
+ return Object.entries(file.projects).map(([repoRoot, config]) => ({
14147
+ repoRoot: path16.resolve(repoRoot),
14148
+ config
14149
+ })).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
14150
+ }
14151
+ function repoLabel(repoRoot) {
14152
+ return path16.basename(repoRoot) || repoRoot;
14153
+ }
14154
+ function resolveCwd2(payload) {
13473
14155
  return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
13474
14156
  }
13475
- function resolveSessionId(payload) {
14157
+ function resolveSessionId2(payload) {
13476
14158
  return payload.session_id ?? payload.conversation_id ?? null;
13477
14159
  }
13478
14160
  function epochPrefix(epoch) {
@@ -13528,27 +14210,43 @@ function canUploadEpochBaselineArtifacts(epoch, artifacts) {
13528
14210
  }
13529
14211
  function pinEpochBaseline(repoRoot, sessionId, epoch) {
13530
14212
  const baselineSha = captureBaselineSha(repoRoot);
14213
+ const baselineRefPrefix = `refs/hillclimb/baseline/${sessionId}/${epochPrefix(epoch)}`;
14214
+ deleteRef(repoRoot, baselineRefPrefix);
14215
+ pinRef(repoRoot, `${baselineRefPrefix}/tracked`, baselineSha);
14216
+ return { baselineSha, headSha: captureHeadSha(repoRoot) };
14217
+ }
14218
+ function pinFrozenBaselineTree(repoRoot, sessionId, epoch, baselineTreeSha) {
14219
+ const prefix = epochPrefix(epoch);
14220
+ const commit = execGit(repoRoot, [
14221
+ "commit-tree",
14222
+ baselineTreeSha,
14223
+ "-m",
14224
+ `frozen baseline ${prefix} for session ${sessionId}`
14225
+ ]);
13531
14226
  pinRef(
13532
14227
  repoRoot,
13533
- `refs/hillclimb/baseline/${sessionId}/${epochPrefix(epoch)}`,
13534
- baselineSha
14228
+ `refs/hillclimb/baseline/${sessionId}/${prefix}/tree`,
14229
+ commit
13535
14230
  );
13536
- return { baselineSha, headSha: captureHeadSha(repoRoot) };
13537
14231
  }
13538
- function buildEpochBaselineArtifacts(params) {
14232
+ function freezeEpochBaseline(params) {
13539
14233
  const {
13540
14234
  repoRoot,
13541
14235
  sessionId,
13542
14236
  tool,
13543
14237
  epoch,
13544
- baselineSha,
13545
14238
  prevHeadSha,
13546
14239
  transitionKind,
13547
14240
  startedAt
13548
14241
  } = params;
13549
14242
  const prefix = epochPrefix(epoch);
13550
14243
  try {
13551
- const metadata = buildBaselineMetadata(
14244
+ const { baselineSha, headSha } = pinEpochBaseline(
14245
+ repoRoot,
14246
+ sessionId,
14247
+ epoch
14248
+ );
14249
+ const baselineMetadata = buildBaselineMetadata(
13552
14250
  repoRoot,
13553
14251
  sessionId,
13554
14252
  tool,
@@ -13560,13 +14258,29 @@ function buildEpochBaselineArtifacts(params) {
13560
14258
  startedAt
13561
14259
  );
13562
14260
  const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha);
14261
+ pinFrozenBaselineTree(repoRoot, sessionId, epoch, baselineTreeSha);
14262
+ return { baselineSha, baselineTreeSha, baselineMetadata, headSha };
14263
+ } catch (err) {
14264
+ appendLog(
14265
+ "error",
14266
+ `git-traces: failed to freeze baseline for ${prefix}: ${formatError(err)}`
14267
+ );
14268
+ return null;
14269
+ }
14270
+ }
14271
+ function buildFrozenEpochBaselineArtifacts(params) {
14272
+ const { repoRoot, sessionId, epoch, baselineTreeSha, baselineMetadata } = params;
14273
+ const prefix = epochPrefix(epoch);
14274
+ try {
13563
14275
  const bundleBuffer = createBundleFromTree(
13564
14276
  repoRoot,
13565
14277
  baselineTreeSha,
13566
14278
  `${sessionId}-${prefix}`,
13567
14279
  `baseline ${prefix}`
13568
14280
  );
13569
- const metadataBuffer = Buffer.from(JSON.stringify(metadata, null, 2));
14281
+ const metadataBuffer = Buffer.from(
14282
+ JSON.stringify(baselineMetadata, null, 2)
14283
+ );
13570
14284
  return { baselineTreeSha, metadataBuffer, bundleBuffer };
13571
14285
  } catch (err) {
13572
14286
  appendLog(
@@ -13576,6 +14290,46 @@ function buildEpochBaselineArtifacts(params) {
13576
14290
  return null;
13577
14291
  }
13578
14292
  }
14293
+ function buildEpochBaselineArtifacts(params) {
14294
+ const {
14295
+ repoRoot,
14296
+ sessionId,
14297
+ tool,
14298
+ epoch,
14299
+ baselineSha,
14300
+ prevHeadSha,
14301
+ transitionKind,
14302
+ startedAt
14303
+ } = params;
14304
+ const prefix = epochPrefix(epoch);
14305
+ try {
14306
+ const metadata = buildBaselineMetadata(
14307
+ repoRoot,
14308
+ sessionId,
14309
+ tool,
14310
+ baselineSha,
14311
+ CLI_VERSION,
14312
+ epoch,
14313
+ prevHeadSha,
14314
+ transitionKind,
14315
+ startedAt
14316
+ );
14317
+ const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha);
14318
+ return buildFrozenEpochBaselineArtifacts({
14319
+ repoRoot,
14320
+ sessionId,
14321
+ epoch,
14322
+ baselineTreeSha,
14323
+ baselineMetadata: metadata
14324
+ });
14325
+ } catch (err) {
14326
+ appendLog(
14327
+ "error",
14328
+ `git-traces: failed to build baseline artifacts for ${prefix}: ${formatError(err)}`
14329
+ );
14330
+ return null;
14331
+ }
14332
+ }
13579
14333
  async function uploadEpochBaselineArtifacts(params) {
13580
14334
  const { client, contributionId, epoch, artifacts } = params;
13581
14335
  const prefix = epochPrefix(epoch);
@@ -13596,6 +14350,34 @@ async function uploadEpochBaselineArtifacts(params) {
13596
14350
  artifacts.metadataBuffer
13597
14351
  );
13598
14352
  }
14353
+ async function createGitTracesContribution(params) {
14354
+ const { client, config, repoRoot, state, tool, now, artifacts } = params;
14355
+ const toolLabel2 = TOOL_LABELS[tool] ?? "Claude";
14356
+ const epochSeconds = formatEpochSeconds3(now);
14357
+ const shortId = state.sessionId.slice(0, 12);
14358
+ const repoName = repoLabel(repoRoot);
14359
+ const contribution = await client.createContribution(config.projectId, {
14360
+ contributionTypeSlug: GIT_TRACES_SLUG,
14361
+ title: `${toolLabel2} session ${shortId} \u2014 ${repoName} \u2014 ${epochSeconds}`,
14362
+ body: `Session ID: ${state.sessionId}
14363
+ Tool: ${toolLabel2}
14364
+ Repo: ${repoRoot}
14365
+ Uploaded: ${now.toISOString()}`
14366
+ });
14367
+ const uploaded = await uploadEpochBaselineArtifacts({
14368
+ client,
14369
+ contributionId: contribution.id,
14370
+ epoch: 1,
14371
+ artifacts
14372
+ });
14373
+ if (!uploaded) return null;
14374
+ await client.submitContribution(contribution.id);
14375
+ appendLog(
14376
+ "info",
14377
+ `git-traces: baseline uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contribution.id}, epoch=1, bundleBytes=${artifacts.bundleBuffer.byteLength}, metadataBytes=${artifacts.metadataBuffer.byteLength})`
14378
+ );
14379
+ return contribution.id;
14380
+ }
13599
14381
  async function uploadEpochBaseline(params) {
13600
14382
  const artifacts = buildEpochBaselineArtifacts(params);
13601
14383
  if (!artifacts) return null;
@@ -13619,161 +14401,263 @@ async function openEpoch(params) {
13619
14401
  if (!uploaded) return null;
13620
14402
  return { baselineSha, baselineTreeSha: uploaded.baselineTreeSha, headSha };
13621
14403
  }
13622
- async function initializeSession(cwd, tool, sessionId) {
13623
- const project = await findProjectForCwd(cwd);
13624
- if (!project) {
13625
- appendLog(
13626
- "warn",
13627
- "git-traces: no hillclimb project config found, skipping"
13628
- );
13629
- return null;
13630
- }
13631
- const { baselineSha, headSha } = pinEpochBaseline(cwd, sessionId, 1);
14404
+ async function initializeSession(repoRoot, tool, sessionId) {
14405
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
14406
+ const frozen = freezeEpochBaseline({
14407
+ repoRoot,
14408
+ sessionId,
14409
+ tool,
14410
+ epoch: 1,
14411
+ prevHeadSha: null,
14412
+ transitionKind: "initial",
14413
+ startedAt
14414
+ });
14415
+ if (!frozen) return null;
13632
14416
  const state = {
13633
- schemaVersion: CURRENT_SCHEMA_VERSION,
14417
+ schemaVersion: CURRENT_SCHEMA_VERSION2,
13634
14418
  sessionId,
13635
14419
  contributionId: null,
13636
- baselineSha,
13637
- baselineTreeSha: null,
13638
- lastSnapshotSha: baselineSha,
13639
- lastSnapshotTreeSha: null,
13640
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
14420
+ baselineSha: frozen.baselineSha,
14421
+ baselineTreeSha: frozen.baselineTreeSha,
14422
+ baselineMetadata: frozen.baselineMetadata,
14423
+ lastSnapshotSha: frozen.baselineSha,
14424
+ lastSnapshotTreeSha: frozen.baselineTreeSha,
14425
+ startedAt,
13641
14426
  epoch: 1,
13642
14427
  turnCount: 0,
13643
- headSha,
13644
- repoRoot: cwd
14428
+ headSha: frozen.headSha,
14429
+ repoRoot
13645
14430
  };
13646
14431
  await writeSessionState(state, tool);
13647
14432
  appendLog(
13648
14433
  "info",
13649
- `git-traces: session pending (baseline=${baselineSha.slice(0, 8)}, awaiting first turn)`
14434
+ `git-traces: session pending (repo=${repoRoot}, baseline=${frozen.baselineSha.slice(0, 8)}, awaiting first turn)`
13650
14435
  );
13651
14436
  return state;
13652
14437
  }
14438
+ async function cleanupStaleScopedSessionStates(repoRoot, tool, currentSessionId, options = {}) {
14439
+ const nowMs = options.nowMs ?? Date.now();
14440
+ const ttlMs = options.ttlMs ?? STALE_SCOPED_STATE_TTL_MS;
14441
+ const states = await listScopedSessionStates(repoRoot, tool);
14442
+ let removed = 0;
14443
+ for (const { state, mtimeMs } of states) {
14444
+ if (state.sessionId === currentSessionId) continue;
14445
+ const startedAtMs = Date.parse(state.startedAt);
14446
+ const ageBaseMs = Number.isNaN(startedAtMs) ? mtimeMs : startedAtMs;
14447
+ if (nowMs - ageBaseMs <= ttlMs) continue;
14448
+ appendLog(
14449
+ "info",
14450
+ `git-traces: cleaning up stale scoped session ${state.sessionId}`
14451
+ );
14452
+ cleanupSessionRefs(repoRoot, state.sessionId);
14453
+ await deleteSessionState(repoRoot, tool, state.sessionId);
14454
+ removed++;
14455
+ }
14456
+ return removed;
14457
+ }
14458
+ async function processSessionStartRepo(repo, tool, sessionId) {
14459
+ const { repoRoot } = repo;
14460
+ if (!isGitRepo(repoRoot)) {
14461
+ appendLog(
14462
+ "info",
14463
+ `git-traces: skipping repo on SessionStart (repo=${repoRoot}, reason=not-git-repo)`
14464
+ );
14465
+ return "skipped";
14466
+ }
14467
+ await acquireLock2(repoRoot, tool);
14468
+ try {
14469
+ const staleLegacy = await readSessionState(repoRoot, tool);
14470
+ if (staleLegacy && staleLegacy.sessionId !== sessionId) {
14471
+ appendLog(
14472
+ "info",
14473
+ `git-traces: cleaning up stale session ${staleLegacy.sessionId} (repo=${repoRoot})`
14474
+ );
14475
+ cleanupSessionRefs(repoRoot, staleLegacy.sessionId);
14476
+ await deleteSessionState(repoRoot, tool);
14477
+ }
14478
+ const staleCount = await cleanupStaleScopedSessionStates(
14479
+ repoRoot,
14480
+ tool,
14481
+ sessionId
14482
+ );
14483
+ if (staleCount > 0) {
14484
+ appendLog(
14485
+ "info",
14486
+ `git-traces: cleaned stale scoped sessions (repo=${repoRoot}, count=${staleCount})`
14487
+ );
14488
+ }
14489
+ const state = await initializeSession(repoRoot, tool, sessionId);
14490
+ return state ? "initialized" : "failed";
14491
+ } catch (err) {
14492
+ appendLog(
14493
+ "error",
14494
+ `git-traces: SessionStart failed for repo ${repoRoot}: ${formatError(err)}`
14495
+ );
14496
+ return "failed";
14497
+ } finally {
14498
+ await releaseLock2(repoRoot, tool);
14499
+ }
14500
+ }
13653
14501
  async function handleSessionStart(payload, tool) {
13654
- const cwd = resolveCwd(payload);
14502
+ const cwd = resolveCwd2(payload);
13655
14503
  if (!cwd) {
13656
14504
  appendLog("warn", "git-traces: no cwd in payload, skipping");
13657
14505
  return;
13658
14506
  }
13659
- if (!isGitRepo(cwd)) {
13660
- appendLog("info", "git-traces: not a git repo, skipping");
14507
+ const startingProject = await findProjectForCwd(cwd);
14508
+ if (!startingProject) {
14509
+ appendLog(
14510
+ "warn",
14511
+ "git-traces: no hillclimb project config found, skipping"
14512
+ );
13661
14513
  return;
13662
14514
  }
13663
- const sessionId = resolveSessionId(payload);
14515
+ const sessionId = resolveSessionId2(payload);
13664
14516
  if (!sessionId) {
13665
14517
  appendLog("warn", "git-traces: no session_id in payload, skipping");
13666
14518
  return;
13667
14519
  }
13668
- await acquireLock(cwd, tool);
13669
- try {
13670
- const stale = await readSessionState(cwd, tool);
13671
- if (stale && stale.sessionId !== sessionId) {
13672
- appendLog(
13673
- "info",
13674
- `git-traces: cleaning up stale session ${stale.sessionId}`
13675
- );
13676
- cleanupSessionRefs(cwd, stale.sessionId);
13677
- await deleteSessionState(cwd, tool);
13678
- }
13679
- await initializeSession(cwd, tool, sessionId);
13680
- } finally {
13681
- await releaseLock(cwd, tool);
14520
+ const repos = await loadConfiguredRepos();
14521
+ let initialized = 0;
14522
+ let skipped = 0;
14523
+ let failed = 0;
14524
+ for (const repo of repos) {
14525
+ const outcome = await processSessionStartRepo(repo, tool, sessionId);
14526
+ if (outcome === "initialized") initialized++;
14527
+ else if (outcome === "skipped") skipped++;
14528
+ else failed++;
13682
14529
  }
14530
+ appendLog(
14531
+ "info",
14532
+ `git-traces: SessionStart summary (session=${sessionId}, tool=${tool}, triggerRepo=${startingProject.repoRoot}, configured=${repos.length}, initialized=${initialized}, skipped=${skipped}, failed=${failed})`
14533
+ );
13683
14534
  }
13684
- async function handleStop(payload, tool) {
13685
- const cwd = resolveCwd(payload);
13686
- if (!cwd) return;
13687
- if (!isGitRepo(cwd)) return;
13688
- const recordedAt = Date.now();
13689
- await acquireLock(cwd, tool);
13690
- try {
13691
- const state = await readSessionState(cwd, tool);
13692
- if (!state) return;
13693
- const project = await findProjectForCwd(cwd);
13694
- if (!project) return;
13695
- const identity = await loadIdentity(project.config.apiBaseUrl);
13696
- if (!identity) {
13697
- appendLog(
13698
- "warn",
13699
- `git-traces: no saved login for ${project.config.apiBaseUrl}, skipping epoch close`
13700
- );
13701
- return;
13702
- }
13703
- const client = new PlatformClient(
13704
- project.config.apiBaseUrl,
13705
- identity.sessionCookie
14535
+ function buildInitialBaselineArtifactsForState(repoRoot, tool, state) {
14536
+ if (state.baselineTreeSha && state.baselineMetadata) {
14537
+ return buildFrozenEpochBaselineArtifacts({
14538
+ repoRoot,
14539
+ sessionId: state.sessionId,
14540
+ epoch: 1,
14541
+ baselineTreeSha: state.baselineTreeSha,
14542
+ baselineMetadata: state.baselineMetadata
14543
+ });
14544
+ }
14545
+ return buildEpochBaselineArtifacts({
14546
+ repoRoot,
14547
+ sessionId: state.sessionId,
14548
+ tool,
14549
+ epoch: 1,
14550
+ baselineSha: state.baselineSha,
14551
+ prevHeadSha: null,
14552
+ transitionKind: "initial",
14553
+ startedAt: state.startedAt
14554
+ });
14555
+ }
14556
+ async function loadRepoClient(repo) {
14557
+ const identity = await loadIdentity(repo.config.apiBaseUrl);
14558
+ if (!identity) {
14559
+ appendLog(
14560
+ "warn",
14561
+ `git-traces: skipping repo on Stop (repo=${repo.repoRoot}, project=${repo.config.projectId}, reason=no-saved-login, apiBaseUrl=${repo.config.apiBaseUrl})`
13706
14562
  );
13707
- if (state.contributionId === null) {
13708
- const artifacts = buildEpochBaselineArtifacts({
13709
- repoRoot: cwd,
13710
- sessionId: state.sessionId,
13711
- tool,
13712
- epoch: 1,
13713
- baselineSha: state.baselineSha,
13714
- prevHeadSha: null,
13715
- transitionKind: "initial",
13716
- startedAt: state.startedAt
13717
- });
13718
- if (!artifacts) return;
13719
- if (!canUploadEpochBaselineArtifacts(1, artifacts)) return;
13720
- const toolLabel = TOOL_LABELS[tool] ?? "Claude";
13721
- const now = /* @__PURE__ */ new Date();
13722
- const epochSeconds = formatEpochSeconds2(now);
13723
- const shortId = state.sessionId.slice(0, 12);
13724
- const contribution = await client.createContribution(
13725
- project.config.projectId,
13726
- {
13727
- contributionTypeSlug: GIT_TRACES_SLUG,
13728
- title: `${toolLabel} session ${shortId} \u2014 ${epochSeconds}`,
13729
- body: `Session ID: ${state.sessionId}
13730
- Tool: ${toolLabel}
13731
- Repo: ${cwd}
13732
- Uploaded: ${now.toISOString()}`
13733
- }
13734
- );
13735
- const uploaded = await uploadEpochBaselineArtifacts({
13736
- client,
13737
- contributionId: contribution.id,
13738
- epoch: 1,
13739
- artifacts
13740
- });
13741
- if (!uploaded) return;
13742
- await client.submitContribution(contribution.id);
13743
- state.contributionId = contribution.id;
13744
- state.baselineTreeSha = artifacts.baselineTreeSha;
13745
- state.lastSnapshotTreeSha = artifacts.baselineTreeSha;
13746
- await writeSessionState(state, tool);
14563
+ return null;
14564
+ }
14565
+ return new PlatformClient(repo.config.apiBaseUrl, identity.sessionCookie);
14566
+ }
14567
+ async function registerInitialContribution(params) {
14568
+ const { repo, state, tool, client, artifacts } = params;
14569
+ const contributionId = await createGitTracesContribution({
14570
+ client,
14571
+ config: repo.config,
14572
+ repoRoot: repo.repoRoot,
14573
+ state,
14574
+ tool,
14575
+ now: /* @__PURE__ */ new Date(),
14576
+ artifacts
14577
+ });
14578
+ if (!contributionId) return false;
14579
+ state.contributionId = contributionId;
14580
+ state.baselineTreeSha = artifacts.baselineTreeSha;
14581
+ state.lastSnapshotTreeSha = artifacts.baselineTreeSha;
14582
+ await writeSessionState(state, tool);
14583
+ appendLog(
14584
+ "info",
14585
+ `git-traces: session registered on first changed turn (repo=${repo.repoRoot}, project=${repo.config.projectId}, contribution=${contributionId}, epoch=1, baseline=${state.baselineSha.slice(0, 8)})`
14586
+ );
14587
+ return true;
14588
+ }
14589
+ async function processStopRepo(repo, tool, sessionId, recordedAt) {
14590
+ const { repoRoot, config } = repo;
14591
+ if (!isGitRepo(repoRoot)) {
14592
+ appendLog(
14593
+ "info",
14594
+ `git-traces: skipping repo on Stop (repo=${repoRoot}, project=${config.projectId}, reason=not-git-repo)`
14595
+ );
14596
+ return "skipped";
14597
+ }
14598
+ await acquireLock2(repoRoot, tool);
14599
+ let state = null;
14600
+ try {
14601
+ state = await readSessionState(repoRoot, tool, sessionId);
14602
+ if (!state) {
13747
14603
  appendLog(
13748
14604
  "info",
13749
- `git-traces: session registered on first turn (epoch=1, baseline=${state.baselineSha.slice(0, 8)}, contribution=${contribution.id})`
14605
+ `git-traces: skipping repo on Stop (repo=${repoRoot}, project=${config.projectId}, reason=no-active-state)`
13750
14606
  );
14607
+ return "no-state";
13751
14608
  }
13752
- const contributionId = state.contributionId;
13753
- const lastSnapshotTreeSha = state.lastSnapshotTreeSha;
13754
- if (contributionId === null || lastSnapshotTreeSha === null) {
14609
+ const lastSnapshotTreeSha = state.lastSnapshotTreeSha ?? state.baselineTreeSha;
14610
+ if (!lastSnapshotTreeSha) {
13755
14611
  appendLog(
13756
14612
  "error",
13757
- "git-traces: invariant violation \u2014 session state missing contributionId or tree SHA after lazy init"
14613
+ `git-traces: invariant violation \u2014 session state missing snapshot tree SHA (repo=${repoRoot}, session=${state.sessionId})`
13758
14614
  );
13759
- return;
14615
+ return "failed";
13760
14616
  }
13761
- const currentHeadSha = captureHeadSha(cwd);
14617
+ const currentHeadSha = captureHeadSha(repoRoot);
13762
14618
  if (currentHeadSha !== state.headSha) {
14619
+ const client2 = await loadRepoClient(repo);
14620
+ if (!client2) return "skipped";
14621
+ if (state.contributionId === null) {
14622
+ const artifacts2 = buildInitialBaselineArtifactsForState(
14623
+ repoRoot,
14624
+ tool,
14625
+ state
14626
+ );
14627
+ if (!artifacts2 || !canUploadEpochBaselineArtifacts(1, artifacts2)) {
14628
+ return "skipped";
14629
+ }
14630
+ const registered = await registerInitialContribution({
14631
+ repo,
14632
+ state,
14633
+ tool,
14634
+ client: client2,
14635
+ artifacts: artifacts2
14636
+ });
14637
+ if (!registered) return "failed";
14638
+ }
14639
+ const contributionId2 = state.contributionId;
14640
+ if (contributionId2 === null) {
14641
+ appendLog(
14642
+ "error",
14643
+ `git-traces: invariant violation \u2014 missing contribution after registration (repo=${repoRoot}, session=${state.sessionId})`
14644
+ );
14645
+ return "failed";
14646
+ }
13763
14647
  const transitionKind = detectTransitionKind(
13764
- cwd,
14648
+ repoRoot,
13765
14649
  state.headSha || null,
13766
14650
  currentHeadSha
13767
14651
  );
13768
14652
  const nextEpoch = state.epoch + 1;
13769
14653
  appendLog(
13770
14654
  "info",
13771
- `git-traces: HEAD moved (${transitionKind}, ${state.headSha.slice(0, 8)} \u2192 ${currentHeadSha.slice(0, 8)}), opening epoch ${nextEpoch}`
14655
+ `git-traces: HEAD moved (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId2}, transition=${transitionKind}, from=${state.headSha.slice(0, 8)}, to=${currentHeadSha.slice(0, 8)}, openingEpoch=${nextEpoch})`
13772
14656
  );
13773
14657
  const artifacts = await openEpoch({
13774
- repoRoot: cwd,
13775
- client,
13776
- contributionId,
14658
+ repoRoot,
14659
+ client: client2,
14660
+ contributionId: contributionId2,
13777
14661
  sessionId: state.sessionId,
13778
14662
  tool,
13779
14663
  epoch: nextEpoch,
@@ -13781,7 +14665,7 @@ Uploaded: ${now.toISOString()}`
13781
14665
  transitionKind,
13782
14666
  startedAt: state.startedAt
13783
14667
  });
13784
- if (!artifacts) return;
14668
+ if (!artifacts) return "failed";
13785
14669
  const next = {
13786
14670
  ...state,
13787
14671
  baselineSha: artifacts.baselineSha,
@@ -13793,65 +14677,228 @@ Uploaded: ${now.toISOString()}`
13793
14677
  headSha: artifacts.headSha
13794
14678
  };
13795
14679
  await writeSessionState(next, tool);
13796
- return;
14680
+ appendLog(
14681
+ "info",
14682
+ `git-traces: epoch baseline uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId2}, epoch=${nextEpoch}, baseline=${artifacts.baselineSha.slice(0, 8)})`
14683
+ );
14684
+ return "uploaded";
13797
14685
  }
13798
- const currentSha = captureSnapshotSha(cwd);
13799
- const currentTreeSha = buildSnapshotTree(cwd, currentSha);
14686
+ const currentSha = captureSnapshotSha(repoRoot);
14687
+ const currentTreeSha = buildSnapshotTree(repoRoot, currentSha);
13800
14688
  const patchBuffer = createTreeDiffPatchGz(
13801
- cwd,
14689
+ repoRoot,
13802
14690
  lastSnapshotTreeSha,
13803
14691
  currentTreeSha
13804
14692
  );
13805
14693
  if (!patchBuffer) {
13806
14694
  appendLog(
13807
14695
  "info",
13808
- "git-traces: turn produced identical snapshot tree, skipping upload"
14696
+ `git-traces: turn produced identical snapshot tree, skipping upload (repo=${repoRoot}, project=${config.projectId})`
13809
14697
  );
13810
- return;
14698
+ return "unchanged";
13811
14699
  }
13812
- state.turnCount++;
13813
14700
  const prefix = epochPrefix(state.epoch);
13814
- const turnLabel = turnSuffix(state.turnCount);
14701
+ const nextTurnCount = state.turnCount + 1;
14702
+ const turnLabel = turnSuffix(nextTurnCount);
14703
+ const filename = `${prefix}-${turnLabel}-${recordedAt}.patch.gz`;
14704
+ if (state.contributionId === null && !canUploadFile(filename, patchBuffer)) {
14705
+ appendLog(
14706
+ "warn",
14707
+ `git-traces: first changed turn skipped before contribution creation (repo=${repoRoot}, project=${config.projectId}, reason=patch-too-large)`
14708
+ );
14709
+ return "skipped";
14710
+ }
14711
+ const client = await loadRepoClient(repo);
14712
+ if (!client) return "skipped";
14713
+ if (state.contributionId === null) {
14714
+ const artifacts = buildInitialBaselineArtifactsForState(
14715
+ repoRoot,
14716
+ tool,
14717
+ state
14718
+ );
14719
+ if (!artifacts || !canUploadEpochBaselineArtifacts(1, artifacts)) {
14720
+ return "skipped";
14721
+ }
14722
+ const registered = await registerInitialContribution({
14723
+ repo,
14724
+ state,
14725
+ tool,
14726
+ client,
14727
+ artifacts
14728
+ });
14729
+ if (!registered) return "failed";
14730
+ }
14731
+ const contributionId = state.contributionId;
14732
+ if (contributionId === null) {
14733
+ appendLog(
14734
+ "error",
14735
+ `git-traces: invariant violation \u2014 missing contribution before patch upload (repo=${repoRoot}, session=${state.sessionId})`
14736
+ );
14737
+ return "failed";
14738
+ }
13815
14739
  pinRef(
13816
- cwd,
14740
+ repoRoot,
13817
14741
  `refs/hillclimb/turns/${state.sessionId}/${prefix}/${turnLabel}`,
13818
14742
  currentSha
13819
14743
  );
13820
- const filename = `${prefix}-${turnLabel}-${recordedAt}.patch.gz`;
13821
- await uploadFile(
14744
+ const uploaded = await uploadFile(
13822
14745
  client,
13823
14746
  contributionId,
13824
14747
  filename,
13825
14748
  "application/gzip",
13826
14749
  patchBuffer
13827
14750
  );
14751
+ if (!uploaded) {
14752
+ appendLog(
14753
+ "warn",
14754
+ `git-traces: ${prefix}-${turnLabel} not uploaded; keeping last uploaded snapshot unchanged (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId})`
14755
+ );
14756
+ return "skipped";
14757
+ }
14758
+ state.turnCount = nextTurnCount;
13828
14759
  state.lastSnapshotSha = currentSha;
13829
14760
  state.lastSnapshotTreeSha = currentTreeSha;
13830
14761
  await writeSessionState(state, tool);
13831
14762
  appendLog(
13832
14763
  "info",
13833
- `git-traces: ${prefix}-${turnLabel} uploaded (${patchBuffer.byteLength} bytes)`
14764
+ `git-traces: patch uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId}, epoch=${state.epoch}, turn=${turnLabel}, bytes=${patchBuffer.byteLength})`
14765
+ );
14766
+ return "uploaded";
14767
+ } catch (err) {
14768
+ appendLog(
14769
+ "error",
14770
+ `git-traces: Stop failed for repo ${repoRoot}: ${formatError(err)}`
13834
14771
  );
14772
+ if (state && err instanceof PlatformError && err.status === 404 && err.code === "CONTRIBUTION_NOT_FOUND") {
14773
+ if (state.baselineTreeSha) {
14774
+ await writeSessionState(
14775
+ {
14776
+ ...state,
14777
+ contributionId: null,
14778
+ lastSnapshotSha: state.baselineSha,
14779
+ lastSnapshotTreeSha: state.baselineTreeSha,
14780
+ turnCount: 0
14781
+ },
14782
+ tool
14783
+ );
14784
+ appendLog(
14785
+ "warn",
14786
+ `git-traces: cleared stale contribution state (repo=${repoRoot}, project=${config.projectId}, missingContribution=${state.contributionId})`
14787
+ );
14788
+ } else {
14789
+ cleanupSessionRefs(repoRoot, state.sessionId);
14790
+ await deleteSessionState(repoRoot, tool, state.sessionId);
14791
+ appendLog(
14792
+ "warn",
14793
+ `git-traces: deleted stale contribution state without a frozen baseline (repo=${repoRoot}, project=${config.projectId}, missingContribution=${state.contributionId})`
14794
+ );
14795
+ }
14796
+ }
14797
+ return "failed";
13835
14798
  } finally {
13836
- await releaseLock(cwd, tool);
14799
+ await releaseLock2(repoRoot, tool);
13837
14800
  }
13838
14801
  }
13839
- async function handleSessionEnd(payload, tool) {
13840
- const cwd = resolveCwd(payload);
14802
+ async function handleStop(payload, tool) {
14803
+ const cwd = resolveCwd2(payload);
13841
14804
  if (!cwd) return;
13842
- await acquireLock(cwd, tool);
14805
+ const project = await findProjectForCwd(cwd);
14806
+ if (!project) return;
14807
+ const sessionId = resolveSessionId2(payload);
14808
+ const recordedAt = Date.now();
14809
+ const repos = await loadConfiguredRepos();
14810
+ const repoByRoot = new Map(repos.map((repo) => [repo.repoRoot, repo]));
14811
+ const targets = [];
14812
+ let missingConfig = 0;
14813
+ if (sessionId) {
14814
+ const storedStates = await listSessionStatesForSession(tool, sessionId);
14815
+ for (const { state } of storedStates) {
14816
+ const repo = repoByRoot.get(path16.resolve(state.repoRoot));
14817
+ if (!repo) {
14818
+ missingConfig++;
14819
+ appendLog(
14820
+ "warn",
14821
+ `git-traces: skipping repo on Stop (repo=${state.repoRoot}, session=${sessionId}, reason=missing-config)`
14822
+ );
14823
+ continue;
14824
+ }
14825
+ targets.push(repo);
14826
+ }
14827
+ if (targets.length === 0 && missingConfig === 0) {
14828
+ targets.push({ repoRoot: project.repoRoot, config: project.config });
14829
+ }
14830
+ } else {
14831
+ targets.push({ repoRoot: project.repoRoot, config: project.config });
14832
+ }
14833
+ let uploaded = 0;
14834
+ let unchanged = 0;
14835
+ let skipped = missingConfig;
14836
+ let noState = 0;
14837
+ let failed = 0;
14838
+ for (const repo of targets) {
14839
+ const outcome = await processStopRepo(repo, tool, sessionId, recordedAt);
14840
+ if (outcome === "uploaded") uploaded++;
14841
+ else if (outcome === "unchanged") unchanged++;
14842
+ else if (outcome === "skipped") skipped++;
14843
+ else if (outcome === "no-state") noState++;
14844
+ else failed++;
14845
+ }
14846
+ appendLog(
14847
+ "info",
14848
+ `git-traces: Stop summary (session=${sessionId ?? "<none>"}, tool=${tool}, triggerRepo=${project.repoRoot}, configured=${repos.length}, frozenStates=${targets.length + missingConfig}, uploaded=${uploaded}, unchanged=${unchanged}, skipped=${skipped}, noState=${noState}, failed=${failed})`
14849
+ );
14850
+ }
14851
+ async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
14852
+ await acquireLock2(repoRoot, tool);
13843
14853
  try {
13844
- const state = await readSessionState(cwd, tool);
13845
- if (!state) return;
13846
- cleanupSessionRefs(cwd, state.sessionId);
13847
- await deleteSessionState(cwd, tool);
14854
+ const state = await readSessionState(repoRoot, tool, sessionId);
14855
+ if (!state) return "no-state";
14856
+ cleanupSessionRefs(repoRoot, state.sessionId);
14857
+ await deleteSessionState(repoRoot, tool, state.sessionId);
13848
14858
  appendLog(
13849
14859
  "info",
13850
- `git-traces: session ${state.sessionId} cleaned up (${state.epoch} epoch(s), last had ${state.turnCount} turns)`
14860
+ `git-traces: session ${state.sessionId} cleaned up (repo=${repoRoot}, epochCount=${state.epoch}, lastTurnCount=${state.turnCount})`
14861
+ );
14862
+ return "cleaned";
14863
+ } catch (err) {
14864
+ appendLog(
14865
+ "error",
14866
+ `git-traces: SessionEnd cleanup failed for repo ${repoRoot}: ${formatError(err)}`
13851
14867
  );
14868
+ return "failed";
13852
14869
  } finally {
13853
- await releaseLock(cwd, tool);
14870
+ await releaseLock2(repoRoot, tool);
14871
+ }
14872
+ }
14873
+ async function handleSessionEnd(payload, tool) {
14874
+ const cwd = resolveCwd2(payload);
14875
+ const sessionId = resolveSessionId2(payload);
14876
+ const project = cwd ? await findProjectForCwd(cwd) : null;
14877
+ const triggerRepo = project?.repoRoot ?? cwd ?? "<none>";
14878
+ const repoRoots = [];
14879
+ if (sessionId) {
14880
+ const states = await listSessionStatesForSession(tool, sessionId);
14881
+ for (const { state } of states) {
14882
+ repoRoots.push(path16.resolve(state.repoRoot));
14883
+ }
14884
+ }
14885
+ if (repoRoots.length === 0 && cwd) {
14886
+ repoRoots.push(project?.repoRoot ?? cwd);
14887
+ }
14888
+ if (repoRoots.length === 0) return;
14889
+ let cleaned = 0;
14890
+ let noState = 0;
14891
+ let failed = 0;
14892
+ for (const repoRoot of repoRoots) {
14893
+ const outcome = await cleanupSessionStateForRepo(repoRoot, tool, sessionId);
14894
+ if (outcome === "cleaned") cleaned++;
14895
+ else if (outcome === "no-state") noState++;
14896
+ else failed++;
13854
14897
  }
14898
+ appendLog(
14899
+ "info",
14900
+ `git-traces: SessionEnd summary (session=${sessionId ?? "<none>"}, tool=${tool}, triggerRepo=${triggerRepo}, states=${repoRoots.length}, cleaned=${cleaned}, noState=${noState}, failed=${failed})`
14901
+ );
13855
14902
  }
13856
14903
 
13857
14904
  // src/git-traces/index.ts
@@ -13859,7 +14906,7 @@ var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
13859
14906
  var TOOL_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_TOOL";
13860
14907
  var FLOW_ID_ENV = "HILLCLIMB_GIT_TRACES_FLOW";
13861
14908
  function newFlowId() {
13862
- return crypto2.randomBytes(3).toString("hex");
14909
+ return crypto3.randomBytes(3).toString("hex");
13863
14910
  }
13864
14911
  var KNOWN_TOOLS = /* @__PURE__ */ new Set([
13865
14912
  "claude",
@@ -13868,6 +14915,25 @@ var KNOWN_TOOLS = /* @__PURE__ */ new Set([
13868
14915
  "cursor",
13869
14916
  "opencode"
13870
14917
  ]);
14918
+ function classifyHookEvent2(event) {
14919
+ switch (event) {
14920
+ case "SessionStart":
14921
+ case "sessionStart":
14922
+ case "session.created":
14923
+ return "sessionStart";
14924
+ case "Stop":
14925
+ case "stop":
14926
+ case "session.idle":
14927
+ return "stop";
14928
+ case "SessionEnd":
14929
+ case "sessionEnd":
14930
+ case "session.deleted":
14931
+ case "server.instance.disposed":
14932
+ return "sessionEnd";
14933
+ default:
14934
+ return "unknown";
14935
+ }
14936
+ }
13871
14937
  function parseToolArg2(argv) {
13872
14938
  for (let i = 0; i < argv.length; i++) {
13873
14939
  const a = argv[i];
@@ -13884,28 +14950,94 @@ async function readStdin2() {
13884
14950
  }
13885
14951
  return Buffer.concat(chunks).toString("utf-8");
13886
14952
  }
13887
- function resolveCwd2(payload) {
14953
+ function resolveCwd3(payload) {
13888
14954
  return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
13889
14955
  }
14956
+ async function repairHookForTool(repoRoot, tool) {
14957
+ const result = await healHookForTool(repoRoot, tool);
14958
+ if (result.skipped) {
14959
+ appendLog("warn", `self-heal: skipped ${tool} hook (${result.skipped})`);
14960
+ return;
14961
+ }
14962
+ if (result.changed) {
14963
+ appendLog("info", `self-heal: updated ${tool} hook`);
14964
+ }
14965
+ }
13890
14966
  async function selfHealHook2(payload, tool) {
13891
- const cwd = resolveCwd2(payload);
14967
+ if (process.env.HILLCLIMB_SKIP_HOOK_SELF_HEAL === "1") return;
14968
+ const cwd = resolveCwd3(payload);
13892
14969
  if (!cwd) return;
13893
14970
  try {
13894
14971
  const project = await findProjectForCwd(cwd);
13895
14972
  if (!project) return;
13896
- const result = await healHookForTool(project.repoRoot, tool);
13897
- if (result.skipped) {
13898
- appendLog("warn", `self-heal: skipped ${tool} hook (${result.skipped})`);
13899
- return;
14973
+ await repairHookForTool(project.repoRoot, tool);
14974
+ } catch (err) {
14975
+ appendLog(
14976
+ "warn",
14977
+ `self-heal: failed to repair ${tool} hook: ${formatError(err)}`
14978
+ );
14979
+ }
14980
+ }
14981
+ async function resolveLegacyBareTool(raw) {
14982
+ let payload;
14983
+ try {
14984
+ payload = JSON.parse(raw);
14985
+ } catch (err) {
14986
+ appendLog(
14987
+ "error",
14988
+ `git-traces: missing --tool and failed to parse payload for legacy hook repair: ${formatError(err)}`
14989
+ );
14990
+ return null;
14991
+ }
14992
+ const cwd = resolveCwd3(payload);
14993
+ if (!cwd) {
14994
+ appendLog(
14995
+ "error",
14996
+ "git-traces: missing --tool and no cwd in payload for legacy hook repair"
14997
+ );
14998
+ return null;
14999
+ }
15000
+ try {
15001
+ const project = await findProjectForCwd(cwd);
15002
+ if (!project) {
15003
+ appendLog(
15004
+ "error",
15005
+ `git-traces: missing --tool and no hillclimb config for cwd ${cwd}`
15006
+ );
15007
+ return null;
13900
15008
  }
13901
- if (result.changed) {
13902
- appendLog("info", `self-heal: updated ${tool} hook`);
15009
+ const owners = await findLegacyGitTracesHookOwners(
15010
+ project.repoRoot,
15011
+ payload.hook_event_name
15012
+ );
15013
+ if (owners.length === 0) {
15014
+ appendLog(
15015
+ "error",
15016
+ `git-traces: missing --tool and no legacy bare git-traces hook matched event ${payload.hook_event_name ?? "<none>"}`
15017
+ );
15018
+ return null;
15019
+ }
15020
+ for (const owner of owners) {
15021
+ await repairHookForTool(project.repoRoot, owner);
15022
+ }
15023
+ if (owners.length > 1) {
15024
+ appendLog(
15025
+ "warn",
15026
+ `git-traces: legacy bare hook matched multiple tools (${owners.join(", ")}); repaired hooks but skipped this event`
15027
+ );
15028
+ return null;
13903
15029
  }
15030
+ appendLog(
15031
+ "info",
15032
+ `git-traces: inferred legacy bare hook owner ${owners[0]} and repaired hook`
15033
+ );
15034
+ return owners[0];
13904
15035
  } catch (err) {
13905
15036
  appendLog(
13906
15037
  "warn",
13907
- `self-heal: failed to repair ${tool} hook: ${formatError(err)}`
15038
+ `git-traces: failed legacy hook repair: ${formatError(err)}`
13908
15039
  );
15040
+ return null;
13909
15041
  }
13910
15042
  }
13911
15043
  async function runGitTraces() {
@@ -13920,13 +15052,6 @@ async function runGitTraces() {
13920
15052
  "info",
13921
15053
  `git-traces hook invoked (pid ${process.pid}, tool=${toolArg ?? "<none>"})`
13922
15054
  );
13923
- if (!toolArg || !KNOWN_TOOLS.has(toolArg)) {
13924
- appendLog(
13925
- "error",
13926
- `git-traces: missing or unknown --tool arg (got ${toolArg ?? "<none>"})`
13927
- );
13928
- return;
13929
- }
13930
15055
  let raw;
13931
15056
  try {
13932
15057
  raw = await readStdin2();
@@ -13938,6 +15063,15 @@ async function runGitTraces() {
13938
15063
  appendLog("warn", "git-traces: empty stdin, expected hook payload");
13939
15064
  return;
13940
15065
  }
15066
+ let tool = toolArg;
15067
+ if (tool && !KNOWN_TOOLS.has(tool)) {
15068
+ appendLog("error", `git-traces: unknown --tool arg (got ${tool})`);
15069
+ return;
15070
+ }
15071
+ if (!tool) {
15072
+ tool = await resolveLegacyBareTool(raw);
15073
+ if (!tool) return;
15074
+ }
13941
15075
  const entrypoint = process.argv[1];
13942
15076
  if (!entrypoint) {
13943
15077
  appendLog("error", "git-traces: process.argv[1] is empty");
@@ -13946,14 +15080,14 @@ async function runGitTraces() {
13946
15080
  try {
13947
15081
  const child = spawn3(
13948
15082
  process.execPath,
13949
- [entrypoint, "git-traces", `--tool=${toolArg}`],
15083
+ [entrypoint, "git-traces", `--tool=${tool}`],
13950
15084
  {
13951
15085
  detached: true,
13952
15086
  stdio: ["pipe", "ignore", "ignore"],
13953
15087
  env: {
13954
15088
  ...process.env,
13955
15089
  [WORKER_ENV_FLAG2]: "1",
13956
- [TOOL_ENV_FLAG2]: toolArg,
15090
+ [TOOL_ENV_FLAG2]: tool,
13957
15091
  [FLOW_ID_ENV]: flowId
13958
15092
  }
13959
15093
  }
@@ -14014,6 +15148,7 @@ async function runGitTracesWorker() {
14014
15148
  return;
14015
15149
  }
14016
15150
  const event = payload.hook_event_name;
15151
+ const eventKind = classifyHookEvent2(event);
14017
15152
  appendLog("info", `git-traces worker: handling event=${event}`);
14018
15153
  if (tool === "claude" && typeof payload.cursor_version === "string") {
14019
15154
  appendLog(
@@ -14022,27 +15157,16 @@ async function runGitTracesWorker() {
14022
15157
  );
14023
15158
  return;
14024
15159
  }
14025
- await selfHealHook2(payload, tool);
14026
15160
  try {
14027
- switch (event) {
14028
- case "SessionStart":
15161
+ await selfHealHook2(payload, tool);
15162
+ switch (eventKind) {
14029
15163
  case "sessionStart":
14030
- // opencode: session.created fires when a new session is first opened.
14031
- case "session.created":
14032
15164
  await handleSessionStart(payload, tool);
14033
15165
  break;
14034
- case "Stop":
14035
15166
  case "stop":
14036
- // opencode: session.idle fires at end-of-turn (agent finished responding).
14037
- case "session.idle":
14038
15167
  await handleStop(payload, tool);
14039
15168
  break;
14040
- case "SessionEnd":
14041
15169
  case "sessionEnd":
14042
- // opencode: server.instance.disposed fires when opencode shuts down.
14043
- // Payload has `cwd` (from the plugin) but no session_id; handleSessionEnd
14044
- // reads state by (repoRoot, tool), so cwd alone is sufficient for cleanup.
14045
- case "server.instance.disposed":
14046
15170
  await handleSessionEnd(payload, tool);
14047
15171
  break;
14048
15172
  default:
@@ -14056,19 +15180,27 @@ async function runGitTracesWorker() {
14056
15180
  `git-traces worker: unexpected error: ${detail}${stack ? `
14057
15181
  ${stack}` : ""}`
14058
15182
  );
15183
+ } finally {
15184
+ if (eventKind === "stop" || eventKind === "sessionEnd") {
15185
+ await recordDebugLogCompletion({
15186
+ kind: "git",
15187
+ tool,
15188
+ payload
15189
+ });
15190
+ }
14059
15191
  }
14060
15192
  }
14061
15193
 
14062
15194
  // src/outputs/zip.ts
14063
- import fs13 from "fs";
14064
- import path16 from "path";
15195
+ import fs14 from "fs";
15196
+ import path18 from "path";
14065
15197
  import archiver2 from "archiver";
14066
15198
 
14067
15199
  // src/outputs/downloads.ts
14068
15200
  import { execSync as execSync2 } from "child_process";
14069
- import fs12 from "fs";
15201
+ import fs13 from "fs";
14070
15202
  import os8 from "os";
14071
- import path15 from "path";
15203
+ import path17 from "path";
14072
15204
  function getDownloadsFolder() {
14073
15205
  const home = os8.homedir();
14074
15206
  if (process.platform === "linux") {
@@ -14077,12 +15209,12 @@ function getDownloadsFolder() {
14077
15209
  encoding: "utf-8",
14078
15210
  timeout: 3e3
14079
15211
  }).trim();
14080
- if (xdgDir && fs12.existsSync(xdgDir)) return xdgDir;
15212
+ if (xdgDir && fs13.existsSync(xdgDir)) return xdgDir;
14081
15213
  } catch {
14082
15214
  }
14083
15215
  }
14084
- const downloads = path15.join(home, "Downloads");
14085
- if (fs12.existsSync(downloads)) return downloads;
15216
+ const downloads = path17.join(home, "Downloads");
15217
+ if (fs13.existsSync(downloads)) return downloads;
14086
15218
  return home;
14087
15219
  }
14088
15220
 
@@ -14091,11 +15223,11 @@ function sanitizeFilename(name) {
14091
15223
  return name.replace(/[^a-zA-Z0-9._-]/g, "_");
14092
15224
  }
14093
15225
  function getUniqueFilename(dir, base, ext) {
14094
- let candidate = path16.join(dir, `${base}${ext}`);
14095
- if (!fs13.existsSync(candidate)) return candidate;
15226
+ let candidate = path18.join(dir, `${base}${ext}`);
15227
+ if (!fs14.existsSync(candidate)) return candidate;
14096
15228
  let i = 1;
14097
- while (fs13.existsSync(candidate)) {
14098
- candidate = path16.join(dir, `${base}-${i}${ext}`);
15229
+ while (fs14.existsSync(candidate)) {
15230
+ candidate = path18.join(dir, `${base}-${i}${ext}`);
14099
15231
  i++;
14100
15232
  }
14101
15233
  return candidate;
@@ -14105,13 +15237,13 @@ var ZipOutput = class {
14105
15237
  label = "Save as .zip to Downloads";
14106
15238
  async emit(group, options) {
14107
15239
  const downloadsDir = getDownloadsFolder();
14108
- const repoName = sanitizeFilename(path16.basename(group.repoPath));
15240
+ const repoName = sanitizeFilename(path18.basename(group.repoPath));
14109
15241
  const timeRange = options.timeRange;
14110
15242
  const rangePart = timeRange?.label ?? "all";
14111
15243
  const epochSeconds = Math.floor(Date.now() / 1e3);
14112
15244
  const baseName = `logs-${repoName}-${rangePart}-${epochSeconds}`;
14113
15245
  const outputPath = getUniqueFilename(downloadsDir, baseName, ".zip");
14114
- const output = fs13.createWriteStream(outputPath);
15246
+ const output = fs14.createWriteStream(outputPath);
14115
15247
  const archive = archiver2("zip", { zlib: { level: 6 } });
14116
15248
  const done = new Promise((resolve, reject) => {
14117
15249
  output.on("close", resolve);
@@ -14305,15 +15437,15 @@ async function confirmExport(group, output) {
14305
15437
  }
14306
15438
 
14307
15439
  // src/sources/claude.ts
14308
- import fs14 from "fs";
15440
+ import fs15 from "fs";
14309
15441
  import os9 from "os";
14310
- import path17 from "path";
15442
+ import path19 from "path";
14311
15443
  import readline from "readline";
14312
15444
  var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
14313
15445
  async function resolveRepoPath(projectDir) {
14314
- const indexPath = path17.join(projectDir, "sessions-index.json");
15446
+ const indexPath = path19.join(projectDir, "sessions-index.json");
14315
15447
  try {
14316
- const raw = await fs14.promises.readFile(indexPath, "utf-8");
15448
+ const raw = await fs15.promises.readFile(indexPath, "utf-8");
14317
15449
  const data = JSON.parse(raw);
14318
15450
  if (data.originalPath && typeof data.originalPath === "string") {
14319
15451
  return data.originalPath;
@@ -14321,12 +15453,12 @@ async function resolveRepoPath(projectDir) {
14321
15453
  } catch {
14322
15454
  }
14323
15455
  const cwdCounts = /* @__PURE__ */ new Map();
14324
- const entries = await fs14.promises.readdir(projectDir, {
15456
+ const entries = await fs15.promises.readdir(projectDir, {
14325
15457
  withFileTypes: true
14326
15458
  });
14327
15459
  for (const entry of entries) {
14328
15460
  if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
14329
- const cwd = await extractCwdFromJsonl(path17.join(projectDir, entry.name));
15461
+ const cwd = await extractCwdFromJsonl(path19.join(projectDir, entry.name));
14330
15462
  if (cwd) {
14331
15463
  cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
14332
15464
  }
@@ -14345,7 +15477,7 @@ async function resolveRepoPath(projectDir) {
14345
15477
  return null;
14346
15478
  }
14347
15479
  async function extractCwdFromJsonl(filePath) {
14348
- const stream = fs14.createReadStream(filePath, { encoding: "utf-8" });
15480
+ const stream = fs15.createReadStream(filePath, { encoding: "utf-8" });
14349
15481
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
14350
15482
  try {
14351
15483
  for await (const line of rl) {
@@ -14367,12 +15499,12 @@ async function extractCwdFromJsonl(filePath) {
14367
15499
  async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
14368
15500
  let entries;
14369
15501
  try {
14370
- entries = await fs14.promises.readdir(dir, { withFileTypes: true });
15502
+ entries = await fs15.promises.readdir(dir, { withFileTypes: true });
14371
15503
  } catch {
14372
15504
  return;
14373
15505
  }
14374
15506
  for (const entry of entries) {
14375
- const fullPath = path17.join(dir, entry.name);
15507
+ const fullPath = path19.join(dir, entry.name);
14376
15508
  if (entry.isDirectory()) {
14377
15509
  if (SKIP_DIRS.has(entry.name)) continue;
14378
15510
  await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
@@ -14394,19 +15526,19 @@ function fallbackDecode(encodedName) {
14394
15526
  var ClaudeSource = class {
14395
15527
  name = "claude";
14396
15528
  async scan() {
14397
- const baseDir = path17.join(os9.homedir(), ".claude", "projects");
15529
+ const baseDir = path19.join(os9.homedir(), ".claude", "projects");
14398
15530
  try {
14399
- await fs14.promises.access(baseDir);
15531
+ await fs15.promises.access(baseDir);
14400
15532
  } catch {
14401
15533
  return [];
14402
15534
  }
14403
- const projectDirs = await fs14.promises.readdir(baseDir, {
15535
+ const projectDirs = await fs15.promises.readdir(baseDir, {
14404
15536
  withFileTypes: true
14405
15537
  });
14406
15538
  const dirEntries = projectDirs.filter((d) => d.isDirectory());
14407
15539
  const resultArrays = await Promise.all(
14408
15540
  dirEntries.map(async (dir) => {
14409
- const projectPath = path17.join(baseDir, dir.name);
15541
+ const projectPath = path19.join(baseDir, dir.name);
14410
15542
  const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
14411
15543
  const files = [];
14412
15544
  await collectFiles(
@@ -14424,12 +15556,12 @@ var ClaudeSource = class {
14424
15556
  };
14425
15557
 
14426
15558
  // src/sources/codex.ts
14427
- import fs15 from "fs";
15559
+ import fs16 from "fs";
14428
15560
  import os10 from "os";
14429
- import path18 from "path";
15561
+ import path20 from "path";
14430
15562
  import readline2 from "readline";
14431
15563
  async function parseSessionMeta(filePath) {
14432
- const stream = fs15.createReadStream(filePath, { encoding: "utf-8" });
15564
+ const stream = fs16.createReadStream(filePath, { encoding: "utf-8" });
14433
15565
  const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
14434
15566
  try {
14435
15567
  for await (const line of rl) {
@@ -14454,12 +15586,12 @@ async function findJsonlFiles(dir) {
14454
15586
  async function walk(d) {
14455
15587
  let entries;
14456
15588
  try {
14457
- entries = await fs15.promises.readdir(d, { withFileTypes: true });
15589
+ entries = await fs16.promises.readdir(d, { withFileTypes: true });
14458
15590
  } catch {
14459
15591
  return;
14460
15592
  }
14461
15593
  for (const entry of entries) {
14462
- const full = path18.join(d, entry.name);
15594
+ const full = path20.join(d, entry.name);
14463
15595
  if (entry.isDirectory()) {
14464
15596
  await walk(full);
14465
15597
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -14473,11 +15605,11 @@ async function findJsonlFiles(dir) {
14473
15605
  async function loadHistory(historyPath) {
14474
15606
  const map = /* @__PURE__ */ new Map();
14475
15607
  try {
14476
- await fs15.promises.access(historyPath);
15608
+ await fs16.promises.access(historyPath);
14477
15609
  } catch {
14478
15610
  return map;
14479
15611
  }
14480
- const stream = fs15.createReadStream(historyPath, { encoding: "utf-8" });
15612
+ const stream = fs16.createReadStream(historyPath, { encoding: "utf-8" });
14481
15613
  const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
14482
15614
  try {
14483
15615
  for await (const line of rl) {
@@ -14504,14 +15636,14 @@ async function loadHistory(historyPath) {
14504
15636
  var CodexSource = class {
14505
15637
  name = "codex";
14506
15638
  async scan() {
14507
- const codexDir = path18.join(os10.homedir(), ".codex");
14508
- const sessionsDir = path18.join(codexDir, "sessions");
15639
+ const codexDir = path20.join(os10.homedir(), ".codex");
15640
+ const sessionsDir = path20.join(codexDir, "sessions");
14509
15641
  try {
14510
- await fs15.promises.access(sessionsDir);
15642
+ await fs16.promises.access(sessionsDir);
14511
15643
  } catch {
14512
15644
  return [];
14513
15645
  }
14514
- const historyPath = path18.join(codexDir, "history.jsonl");
15646
+ const historyPath = path20.join(codexDir, "history.jsonl");
14515
15647
  const [jsonlFiles, historyMap] = await Promise.all([
14516
15648
  findJsonlFiles(sessionsDir),
14517
15649
  loadHistory(historyPath)
@@ -14534,8 +15666,8 @@ var CodexSource = class {
14534
15666
  });
14535
15667
  const historyLines = historyMap.get(meta.sessionId);
14536
15668
  if (historyLines) {
14537
- const sessionDir = path18.relative(sessionsDir, path18.dirname(filePath));
14538
- const historyAbsPath = path18.join(
15669
+ const sessionDir = path20.relative(sessionsDir, path20.dirname(filePath));
15670
+ const historyAbsPath = path20.join(
14539
15671
  sessionsDir,
14540
15672
  sessionDir,
14541
15673
  `history-${meta.sessionId}.jsonl`
@@ -14555,18 +15687,18 @@ var CodexSource = class {
14555
15687
  };
14556
15688
 
14557
15689
  // src/sources/copilotChat.ts
14558
- import fs16 from "fs";
15690
+ import fs17 from "fs";
14559
15691
  import os11 from "os";
14560
- import path19 from "path";
15692
+ import path21 from "path";
14561
15693
  import { fileURLToPath } from "url";
14562
15694
  function vsCodeUserDirs() {
14563
15695
  const home = os11.homedir();
14564
15696
  const dirs = [
14565
- path19.join(home, "Library", "Application Support", "Code", "User"),
14566
- path19.join(home, ".config", "Code", "User")
15697
+ path21.join(home, "Library", "Application Support", "Code", "User"),
15698
+ path21.join(home, ".config", "Code", "User")
14567
15699
  ];
14568
15700
  if (process.env.APPDATA) {
14569
- dirs.push(path19.join(process.env.APPDATA, "Code", "User"));
15701
+ dirs.push(path21.join(process.env.APPDATA, "Code", "User"));
14570
15702
  }
14571
15703
  return dirs;
14572
15704
  }
@@ -14581,7 +15713,7 @@ function uriToFsPath(uri) {
14581
15713
  async function readWorkspaceFolder(workspaceJsonPath) {
14582
15714
  let raw;
14583
15715
  try {
14584
- raw = await fs16.promises.readFile(workspaceJsonPath, "utf-8");
15716
+ raw = await fs17.promises.readFile(workspaceJsonPath, "utf-8");
14585
15717
  } catch {
14586
15718
  return null;
14587
15719
  }
@@ -14603,10 +15735,10 @@ var CopilotChatSource = class {
14603
15735
  async scan() {
14604
15736
  const results = [];
14605
15737
  for (const userDir of vsCodeUserDirs()) {
14606
- const workspaceStorage = path19.join(userDir, "workspaceStorage");
15738
+ const workspaceStorage = path21.join(userDir, "workspaceStorage");
14607
15739
  let hashDirs;
14608
15740
  try {
14609
- hashDirs = await fs16.promises.readdir(workspaceStorage, {
15741
+ hashDirs = await fs17.promises.readdir(workspaceStorage, {
14610
15742
  withFileTypes: true
14611
15743
  });
14612
15744
  } catch {
@@ -14614,22 +15746,22 @@ var CopilotChatSource = class {
14614
15746
  }
14615
15747
  for (const hash of hashDirs) {
14616
15748
  if (!hash.isDirectory()) continue;
14617
- const wsRoot = path19.join(workspaceStorage, hash.name);
14618
- const transcriptsDir = path19.join(
15749
+ const wsRoot = path21.join(workspaceStorage, hash.name);
15750
+ const transcriptsDir = path21.join(
14619
15751
  wsRoot,
14620
15752
  "GitHub.copilot-chat",
14621
15753
  "transcripts"
14622
15754
  );
14623
15755
  let transcriptEntries;
14624
15756
  try {
14625
- transcriptEntries = await fs16.promises.readdir(transcriptsDir, {
15757
+ transcriptEntries = await fs17.promises.readdir(transcriptsDir, {
14626
15758
  withFileTypes: true
14627
15759
  });
14628
15760
  } catch {
14629
15761
  continue;
14630
15762
  }
14631
15763
  const repoPath = await readWorkspaceFolder(
14632
- path19.join(wsRoot, "workspace.json")
15764
+ path21.join(wsRoot, "workspace.json")
14633
15765
  );
14634
15766
  if (!repoPath) continue;
14635
15767
  for (const entry of transcriptEntries) {
@@ -14637,7 +15769,7 @@ var CopilotChatSource = class {
14637
15769
  const sessionId = entry.name.slice(0, -".jsonl".length);
14638
15770
  results.push({
14639
15771
  sourceName: this.name,
14640
- absolutePath: path19.join(transcriptsDir, entry.name),
15772
+ absolutePath: path21.join(transcriptsDir, entry.name),
14641
15773
  repoPath,
14642
15774
  metadata: { sessionId }
14643
15775
  });
@@ -14677,7 +15809,7 @@ function reportRedactionStats(noun, stats) {
14677
15809
  async function filterByTimeRange(group, range) {
14678
15810
  const results = await Promise.all(
14679
15811
  group.files.map(
14680
- (f) => fs17.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
15812
+ (f) => fs18.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
14681
15813
  )
14682
15814
  );
14683
15815
  const filtered = [];
@@ -14704,10 +15836,10 @@ async function runInteractive() {
14704
15836
  s.start(`Scanning ${source.name} logs...`);
14705
15837
  const allFiles = await source.scan();
14706
15838
  const allGroups = await mergeByRepo(allFiles);
14707
- const repoRoot = path20.resolve(repo.root);
15839
+ const repoRoot = path22.resolve(repo.root);
14708
15840
  const matching = allGroups.filter((g) => {
14709
- const resolved = path20.resolve(g.repoPath);
14710
- return resolved === repoRoot || resolved.startsWith(repoRoot + path20.sep);
15841
+ const resolved = path22.resolve(g.repoPath);
15842
+ return resolved === repoRoot || resolved.startsWith(repoRoot + path22.sep);
14711
15843
  });
14712
15844
  if (matching.length === 0) {
14713
15845
  s.stop(`No ${source.name} logs found for ${repo.name}.`);
@@ -14738,7 +15870,7 @@ async function runInteractive() {
14738
15870
  }
14739
15871
  }
14740
15872
  const envFileNames = await discoverEnvFiles(repoRoot);
14741
- const envFilePaths = envFileNames.map((n) => path20.join(repoRoot, n));
15873
+ const envFilePaths = envFileNames.map((n) => path22.join(repoRoot, n));
14742
15874
  const additionalFiles = await promptSecretFiles(envFileNames);
14743
15875
  const secretResult = await collectSecrets(
14744
15876
  repoRoot,