hillclimb 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +1121 -598
  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 path21 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
@@ -2149,9 +2149,14 @@ async function runStatus(args = []) {
2149
2149
 
2150
2150
  // src/commands/upload.ts
2151
2151
  import { spawn as spawn2 } from "child_process";
2152
- import fs9 from "fs";
2152
+ import fs10 from "fs";
2153
2153
  import os5 from "os";
2154
- 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";
2155
2160
 
2156
2161
  // src/middleware/pattern-redact.ts
2157
2162
  import os3 from "os";
@@ -10844,161 +10849,732 @@ async function collectSecrets(repoRoot, envFiles, additionalFiles) {
10844
10849
  return { values, sourceFiles, processEnvCount, skippedCount };
10845
10850
  }
10846
10851
 
10847
- // src/normalizer/index.ts
10848
- import path9 from "path";
10852
+ // src/outputs/platform.ts
10853
+ import { PassThrough } from "stream";
10854
+ import archiver from "archiver";
10849
10855
 
10850
- // src/normalizer/claude.ts
10851
- function stringify(value) {
10852
- if (typeof value === "string") return value;
10853
- try {
10854
- return JSON.stringify(value);
10855
- } catch {
10856
- 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;
10857
10870
  }
10858
10871
  }
10859
- function extractTextReasoningToolUses(content) {
10860
- if (typeof content === "string") {
10861
- return [content.trim(), void 0, []];
10862
- }
10863
- const textParts = [];
10864
- const reasoningParts = [];
10865
- const toolBlocks = [];
10866
- if (Array.isArray(content)) {
10867
- for (const block of content) {
10868
- if (typeof block !== "object" || block === null) {
10869
- textParts.push(stringify(block));
10870
- continue;
10871
- }
10872
- const b = block;
10873
- const blockType = b.type;
10874
- if (blockType === "tool_use") {
10875
- toolBlocks.push(b);
10876
- continue;
10877
- }
10878
- if (blockType === "thinking" || blockType === "reasoning" || blockType === "analysis") {
10879
- const textValue2 = b.text !== void 0 && b.text !== null ? b.text : b.thinking;
10880
- if (typeof textValue2 === "string") {
10881
- reasoningParts.push(textValue2.trim());
10882
- } else {
10883
- reasoningParts.push(stringify(textValue2));
10884
- }
10885
- continue;
10886
- }
10887
- if (blockType === "code" && typeof b.code === "string") {
10888
- textParts.push(b.code);
10889
- continue;
10890
- }
10891
- const textValue = b.text;
10892
- if (typeof textValue === "string") {
10893
- textParts.push(textValue);
10894
- } else {
10895
- textParts.push(stringify(b));
10896
- }
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 });
10897
10882
  }
10898
- } else if (content !== void 0 && content !== null) {
10899
- textParts.push(stringify(content));
10900
10883
  }
10901
- const text3 = textParts.filter((p7) => p7 && p7.trim()).map((p7) => p7.trim()).join("\n\n");
10902
- const reasoning = reasoningParts.filter((p7) => p7 && p7.trim()).map((p7) => p7.trim()).join("\n\n");
10903
- return [text3, reasoning || void 0, toolBlocks];
10904
10884
  }
10905
- function buildMetrics(usage) {
10906
- if (typeof usage !== "object" || usage === null) return void 0;
10907
- const u = usage;
10908
- const cachedTokens = u.cache_read_input_tokens || 0;
10909
- const creation = u.cache_creation_input_tokens || 0;
10910
- const inputTokens = u.input_tokens || 0;
10911
- const promptTokens = inputTokens + cachedTokens + creation;
10912
- const completionTokens = u.output_tokens || 0;
10913
- const extra = {};
10914
- for (const [key, value] of Object.entries(u)) {
10915
- if (key === "input_tokens" || key === "output_tokens") continue;
10916
- extra[key] = value;
10917
- }
10918
- return {
10919
- prompt_tokens: promptTokens,
10920
- completion_tokens: completionTokens,
10921
- cached_tokens: cachedTokens,
10922
- extra: Object.keys(extra).length > 0 ? extra : void 0
10923
- };
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);
10924
10902
  }
10925
- function formatToolResult(block, toolUseResult) {
10926
- const parts = [];
10927
- const content = block.content;
10928
- if (typeof content === "string") {
10929
- if (content.trim()) parts.push(content.trim());
10930
- } else if (Array.isArray(content)) {
10931
- for (const item of content) {
10932
- const text3 = stringify(item);
10933
- if (text3.trim()) parts.push(text3.trim());
10934
- }
10935
- } else if (content !== void 0 && content !== null && content !== "") {
10936
- parts.push(stringify(content));
10903
+ var PlatformUploadOutput = class {
10904
+ constructor(opts) {
10905
+ this.opts = opts;
10937
10906
  }
10938
- let metadata;
10939
- if (toolUseResult && typeof toolUseResult === "object") {
10940
- metadata = { tool_use_result: toolUseResult };
10941
- const stdout = toolUseResult.stdout;
10942
- const stderr = toolUseResult.stderr;
10943
- const exitCode = toolUseResult.exitCode ?? toolUseResult.exit_code;
10944
- const interrupted = toolUseResult.interrupted;
10945
- const isImage = toolUseResult.isImage;
10946
- const formatted = [];
10947
- if (stdout) formatted.push(`[stdout]
10948
- ${stdout}`.trimEnd());
10949
- if (stderr) formatted.push(`[stderr]
10950
- ${stderr}`.trimEnd());
10951
- if (exitCode !== void 0 && exitCode !== null && exitCode !== 0)
10952
- formatted.push(`[exit_code] ${exitCode}`);
10953
- if (interrupted) formatted.push(`[interrupted] ${interrupted}`);
10954
- if (isImage) formatted.push(`[is_image] ${isImage}`);
10955
- const skipKeys = /* @__PURE__ */ new Set([
10956
- "stdout",
10957
- "stderr",
10958
- "exitCode",
10959
- "exit_code",
10960
- "interrupted",
10961
- "isImage"
10962
- ]);
10963
- const remainingMeta = {};
10964
- for (const [k, v] of Object.entries(toolUseResult)) {
10965
- if (!skipKeys.has(k)) remainingMeta[k] = v;
10966
- }
10967
- if (Object.keys(remainingMeta).length > 0) {
10968
- formatted.push(`[metadata] ${JSON.stringify(remainingMeta)}`);
10969
- }
10970
- if (formatted.length > 0) {
10971
- 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`);
10972
10945
  }
10946
+ return contribution.id;
10973
10947
  }
10974
- if (block.is_error === true) {
10975
- parts.push("[error] tool reported failure");
10976
- metadata = metadata || {};
10977
- metadata.is_error = true;
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);
10978
10957
  }
10979
- if (metadata !== void 0) {
10980
- if (!("raw_tool_result" in metadata)) {
10981
- metadata.raw_tool_result = block;
10982
- }
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;
10983
10969
  }
10984
- const resultText = parts.filter(Boolean).join("\n\n").trim();
10985
- return [resultText || void 0, metadata];
10970
+ return repoPath;
10986
10971
  }
10987
- function convertClaudeToTrajectory(jsonlContent, sessionId) {
10988
- const rawEvents = [];
10989
- for (const line of jsonlContent.split("\n")) {
10990
- const trimmed = line.trim();
10991
- if (!trimmed) continue;
10992
- try {
10993
- rawEvents.push(JSON.parse(trimmed));
10994
- } catch {
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 }]);
10995
10981
  }
10996
10982
  }
10997
- if (rawEvents.length === 0) return null;
10998
- const events = rawEvents.sort(
10999
- (a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? "")
11000
- );
11001
- if (events.length === 0) return null;
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];
11562
+ }
11563
+ function convertClaudeToTrajectory(jsonlContent, sessionId) {
11564
+ const rawEvents = [];
11565
+ for (const line of jsonlContent.split("\n")) {
11566
+ const trimmed = line.trim();
11567
+ if (!trimmed) continue;
11568
+ try {
11569
+ rawEvents.push(JSON.parse(trimmed));
11570
+ } catch {
11571
+ }
11572
+ }
11573
+ if (rawEvents.length === 0) return null;
11574
+ const events = rawEvents.sort(
11575
+ (a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? "")
11576
+ );
11577
+ if (events.length === 0) return null;
11002
11578
  let sid = sessionId ?? "";
11003
11579
  for (const event of events) {
11004
11580
  if (typeof event.sessionId === "string") {
@@ -12470,288 +13046,106 @@ function convertRunEventsToTrajectory(events, sessionId) {
12470
13046
  agent: {
12471
13047
  name: "opencode",
12472
13048
  version: "unknown"
12473
- },
12474
- steps,
12475
- final_metrics: finalMetricsFromSteps2(steps)
12476
- };
12477
- }
12478
- function isRunEvent(lines) {
12479
- return lines.some(
12480
- (line) => ["step_start", "step_finish", "text", "reasoning", "tool_use"].includes(
12481
- String(line.type ?? "")
12482
- )
12483
- );
12484
- }
12485
- function convertOpenCodeToTrajectory(content, sessionId) {
12486
- const trimmed = content.trim();
12487
- if (!trimmed) return null;
12488
- try {
12489
- const parsed = JSON.parse(trimmed);
12490
- const parsedObj = asObject3(parsed);
12491
- const messages = asArray(parsedObj?.messages);
12492
- if (parsedObj && messages.length > 0) {
12493
- const entries2 = messages.flatMap((message) => {
12494
- const entry = entryFromLine(asObject3(message) ?? {});
12495
- return entry ? [entry] : [];
12496
- });
12497
- return convertMessageEntriesToTrajectory(
12498
- entries2,
12499
- sessionId,
12500
- asObject3(parsedObj.info)
12501
- );
12502
- }
12503
- } catch {
12504
- }
12505
- const lines = parseJsonLines2(content);
12506
- if (lines.length === 0) return null;
12507
- if (isRunEvent(lines)) return convertRunEventsToTrajectory(lines, sessionId);
12508
- const wrappedEntries = entriesFromEventWrappers(lines);
12509
- const entries = wrappedEntries.length > 0 ? wrappedEntries : lines.flatMap((line) => {
12510
- const entry = entryFromLine(line);
12511
- return entry ? [entry] : [];
12512
- });
12513
- return convertMessageEntriesToTrajectory(entries, sessionId);
12514
- }
12515
-
12516
- // src/normalizer/index.ts
12517
- function normalizeContent(sourceName, content, sessionId) {
12518
- switch (sourceName) {
12519
- case "claude":
12520
- return convertClaudeToTrajectory(content, sessionId);
12521
- case "codex":
12522
- return convertCodexToTrajectory(content, sessionId);
12523
- case "cursor":
12524
- return convertCursorToTrajectory(content, sessionId);
12525
- case "opencode":
12526
- return convertOpenCodeToTrajectory(content, sessionId);
12527
- case "copilot-chat":
12528
- return convertCopilotChatToTrajectory(content, sessionId);
12529
- default:
12530
- return null;
12531
- }
12532
- }
12533
- var NormalizeMiddleware = class {
12534
- name = "normalize";
12535
- async process(group) {
12536
- const newFiles = [];
12537
- for (const file of group.files) {
12538
- newFiles.push(file);
12539
- if (!file.absolutePath.endsWith(".jsonl")) continue;
12540
- if (!["claude", "codex", "cursor", "opencode", "copilot-chat"].includes(
12541
- file.sourceName
12542
- ))
12543
- continue;
12544
- const content = file.content ? file.content.toString("utf-8") : null;
12545
- if (!content) continue;
12546
- const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 : path9.basename(file.absolutePath, ".jsonl"));
12547
- try {
12548
- const trajectory = normalizeContent(
12549
- file.sourceName,
12550
- content,
12551
- sessionId
12552
- );
12553
- if (!trajectory) continue;
12554
- const json = JSON.stringify(
12555
- excludeNone(trajectory),
12556
- null,
12557
- 2
12558
- );
12559
- const atifPath = file.absolutePath.replace(/\.jsonl$/, ".atif.json");
12560
- newFiles.push({
12561
- sourceName: file.sourceName,
12562
- absolutePath: atifPath,
12563
- repoPath: file.repoPath,
12564
- metadata: { ...file.metadata, isAtif: true },
12565
- content: Buffer.from(json, "utf-8")
12566
- });
12567
- } catch {
12568
- }
12569
- }
12570
- return { ...group, files: newFiles };
12571
- }
12572
- };
12573
-
12574
- // src/outputs/platform.ts
12575
- import { PassThrough } from "stream";
12576
- import archiver from "archiver";
12577
-
12578
- // src/outputs/archive.ts
12579
- import os4 from "os";
12580
- import path10 from "path";
12581
- function getSourceBaseDir(sourceName) {
12582
- const home = os4.homedir();
12583
- switch (sourceName) {
12584
- case "claude":
12585
- return path10.join(home, ".claude", "projects");
12586
- case "codex":
12587
- return path10.join(home, ".codex", "sessions");
12588
- default:
12589
- return home;
12590
- }
12591
- }
12592
- function addGroupToArchive(archive, group, selectedSources) {
12593
- for (const file of group.files) {
12594
- if (!selectedSources.has(file.sourceName)) continue;
12595
- const baseDir = getSourceBaseDir(file.sourceName);
12596
- const relativePath = file.absolutePath.startsWith(baseDir) ? path10.relative(baseDir, file.absolutePath) : path10.basename(file.absolutePath);
12597
- const archivePath = path10.join(file.sourceName, relativePath);
12598
- if (file.content) {
12599
- archive.append(file.content, { name: archivePath });
12600
- } else {
12601
- archive.file(file.absolutePath, { name: archivePath });
12602
- }
12603
- }
12604
- }
12605
-
12606
- // src/outputs/platform.ts
12607
- async function buildZipBuffer(group, selectedSources) {
12608
- const archive = archiver("zip", { zlib: { level: 6 } });
12609
- const stream = new PassThrough();
12610
- archive.pipe(stream);
12611
- const chunks = [];
12612
- const done = new Promise((resolve, reject) => {
12613
- stream.on("data", (chunk) => chunks.push(chunk));
12614
- stream.on("end", resolve);
12615
- stream.on("error", reject);
12616
- archive.on("error", reject);
12617
- });
12618
- addGroupToArchive(archive, group, selectedSources);
12619
- await archive.finalize();
12620
- await done;
12621
- return Buffer.concat(chunks);
12622
- }
12623
- var PlatformUploadOutput = class {
12624
- constructor(opts) {
12625
- this.opts = opts;
12626
- }
12627
- name = "platform";
12628
- label = "Upload to hillclimb platform";
12629
- async emit(group, options) {
12630
- const selectedSources = new Set(options.selectedSources);
12631
- const buffer = await buildZipBuffer(group, selectedSources);
12632
- const {
12633
- client,
12634
- projectId,
12635
- contributionTypeSlug,
12636
- contributionTitle,
12637
- contributionBody,
12638
- zipFilename,
12639
- autoSubmit
12640
- } = this.opts;
12641
- const contribution = await client.createContribution(projectId, {
12642
- contributionTypeSlug,
12643
- title: contributionTitle,
12644
- body: contributionBody
12645
- });
12646
- const presigned = await client.createUpload(contribution.id, {
12647
- originalFilename: zipFilename,
12648
- mimeType: "application/zip",
12649
- sizeBytes: buffer.byteLength
12650
- });
12651
- appendLog(
12652
- "info",
12653
- `uploading ${zipFilename} (${buffer.byteLength} bytes) to presigned URL`
12654
- );
12655
- await client.uploadToPresignedUrl(
12656
- presigned.presignedUrl,
12657
- presigned.headers,
12658
- buffer
12659
- );
12660
- appendLog("info", `PUT to presigned URL succeeded for ${zipFilename}`);
12661
- if (autoSubmit) {
12662
- appendLog("info", `submitting contribution ${contribution.id}`);
12663
- await client.submitContribution(contribution.id);
12664
- appendLog("info", `contribution ${contribution.id} submitted`);
12665
- }
12666
- return contribution.id;
12667
- }
12668
- };
12669
-
12670
- // src/pipeline.ts
12671
- import fs8 from "fs";
12672
- import path11 from "path";
12673
- function canonicalizePath(p7) {
12674
- let resolved = path11.resolve(p7);
12675
- if (resolved.endsWith(path11.sep) && resolved !== path11.sep) {
12676
- resolved = resolved.slice(0, -1);
12677
- }
12678
- return resolved;
12679
- }
12680
- function computeLabel(repoPath, allPaths) {
12681
- const segments = repoPath.split(path11.sep).filter(Boolean);
12682
- for (let depth = 1; depth <= segments.length; depth++) {
12683
- const label = segments.slice(-depth).join("/");
12684
- const matches = allPaths.filter((p7) => {
12685
- const s = p7.split(path11.sep).filter(Boolean);
12686
- return s.slice(-depth).join("/") === label;
12687
- });
12688
- if (matches.length === 1) return label;
12689
- }
12690
- return repoPath;
13049
+ },
13050
+ steps,
13051
+ final_metrics: finalMetricsFromSteps2(steps)
13052
+ };
12691
13053
  }
12692
- async function mergeByRepo(files) {
12693
- const grouped = /* @__PURE__ */ new Map();
12694
- for (const file of files) {
12695
- const key = canonicalizePath(file.repoPath);
12696
- const existing = grouped.get(key);
12697
- if (existing) {
12698
- existing.push({ ...file, repoPath: key });
12699
- } else {
12700
- grouped.set(key, [{ ...file, repoPath: key }]);
13054
+ function isRunEvent(lines) {
13055
+ return lines.some(
13056
+ (line) => ["step_start", "step_finish", "text", "reasoning", "tool_use"].includes(
13057
+ String(line.type ?? "")
13058
+ )
13059
+ );
13060
+ }
13061
+ function convertOpenCodeToTrajectory(content, sessionId) {
13062
+ const trimmed = content.trim();
13063
+ if (!trimmed) return null;
13064
+ try {
13065
+ const parsed = JSON.parse(trimmed);
13066
+ const parsedObj = asObject3(parsed);
13067
+ const messages = asArray(parsedObj?.messages);
13068
+ if (parsedObj && messages.length > 0) {
13069
+ const entries2 = messages.flatMap((message) => {
13070
+ const entry = entryFromLine(asObject3(message) ?? {});
13071
+ return entry ? [entry] : [];
13072
+ });
13073
+ return convertMessageEntriesToTrajectory(
13074
+ entries2,
13075
+ sessionId,
13076
+ asObject3(parsedObj.info)
13077
+ );
12701
13078
  }
13079
+ } catch {
12702
13080
  }
12703
- const allPaths = [...grouped.keys()];
12704
- const groups = [];
12705
- for (const [repoPath, groupFiles] of grouped) {
12706
- const stats = await Promise.all(
12707
- groupFiles.map((f) => fs8.promises.stat(f.absolutePath).catch(() => null))
12708
- );
12709
- let lastModified = /* @__PURE__ */ new Date(0);
12710
- for (const stat of stats) {
12711
- if (stat && stat.mtime > lastModified) lastModified = stat.mtime;
12712
- }
12713
- const sourceNames = [...new Set(groupFiles.map((f) => f.sourceName))];
12714
- groups.push({
12715
- repoPath,
12716
- label: computeLabel(repoPath, allPaths),
12717
- files: groupFiles,
12718
- sourceNames,
12719
- lastModified
12720
- });
13081
+ const lines = parseJsonLines2(content);
13082
+ if (lines.length === 0) return null;
13083
+ if (isRunEvent(lines)) return convertRunEventsToTrajectory(lines, sessionId);
13084
+ const wrappedEntries = entriesFromEventWrappers(lines);
13085
+ const entries = wrappedEntries.length > 0 ? wrappedEntries : lines.flatMap((line) => {
13086
+ const entry = entryFromLine(line);
13087
+ return entry ? [entry] : [];
13088
+ });
13089
+ return convertMessageEntriesToTrajectory(entries, sessionId);
13090
+ }
13091
+
13092
+ // src/normalizer/index.ts
13093
+ function normalizeContent(sourceName, content, sessionId) {
13094
+ switch (sourceName) {
13095
+ case "claude":
13096
+ return convertClaudeToTrajectory(content, sessionId);
13097
+ case "codex":
13098
+ return convertCodexToTrajectory(content, sessionId);
13099
+ case "cursor":
13100
+ return convertCursorToTrajectory(content, sessionId);
13101
+ case "opencode":
13102
+ return convertOpenCodeToTrajectory(content, sessionId);
13103
+ case "copilot-chat":
13104
+ return convertCopilotChatToTrajectory(content, sessionId);
13105
+ default:
13106
+ return null;
12721
13107
  }
12722
- groups.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
12723
- return groups.filter((g) => g.files.length > 0);
12724
13108
  }
12725
- async function preloadFiles(group) {
12726
- const files = await Promise.all(
12727
- group.files.map(async (file) => {
12728
- 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"));
12729
13123
  try {
12730
- const buf = await fs8.promises.readFile(file.absolutePath);
12731
- const checkLen = Math.min(buf.length, 8192);
12732
- for (let i = 0; i < checkLen; i++) {
12733
- if (buf[i] === 0) {
12734
- return { ...file, content: buf, isBinary: true };
12735
- }
12736
- }
12737
- 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
+ });
12738
13143
  } catch {
12739
- return file;
12740
13144
  }
12741
- })
12742
- );
12743
- return { ...group, files };
12744
- }
12745
- async function runPipeline(group, middleware2, output, options, onProgress) {
12746
- const preloaded = await preloadFiles(group);
12747
- let processed = preloaded;
12748
- for (const mw of middleware2) {
12749
- onProgress?.(`${mw.name} (${processed.files.length} files)`);
12750
- processed = await mw.process(processed);
13145
+ }
13146
+ return { ...group, files: newFiles };
12751
13147
  }
12752
- onProgress?.(`Compressing ${processed.files.length} files`);
12753
- return output.emit(processed, options);
12754
- }
13148
+ };
12755
13149
 
12756
13150
  // src/commands/upload.ts
12757
13151
  async function readStdin() {
@@ -12762,17 +13156,17 @@ async function readStdin() {
12762
13156
  }
12763
13157
  return Buffer.concat(chunks).toString("utf-8");
12764
13158
  }
12765
- function sanitize(value) {
13159
+ function sanitize2(value) {
12766
13160
  return value.replace(/[^a-zA-Z0-9._-]/g, "_");
12767
13161
  }
12768
- function formatEpochSeconds(date) {
13162
+ function formatEpochSeconds2(date) {
12769
13163
  return String(Math.floor(date.getTime() / 1e3));
12770
13164
  }
12771
13165
  function lineHasAssistant(line) {
12772
13166
  return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
12773
13167
  }
12774
13168
  async function hasAssistantMessage(transcriptPath) {
12775
- const stream = fs9.createReadStream(transcriptPath, { encoding: "utf-8" });
13169
+ const stream = fs10.createReadStream(transcriptPath, { encoding: "utf-8" });
12776
13170
  let buffer = "";
12777
13171
  try {
12778
13172
  for await (const chunk of stream) {
@@ -12800,6 +13194,23 @@ function resolveSourceTool(payload) {
12800
13194
  if (payload.hook_event_name === "Stop") return "codex";
12801
13195
  return "claude";
12802
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
+ }
12803
13214
  async function selfHealHook(repoRoot, tool) {
12804
13215
  if (process.env.HILLCLIMB_SKIP_HOOK_SELF_HEAL === "1") return;
12805
13216
  try {
@@ -12823,7 +13234,7 @@ function resolveCursorTranscriptPath(payload) {
12823
13234
  const workspace = payload.workspace_roots?.[0];
12824
13235
  if (!id || !workspace) return void 0;
12825
13236
  const encoded = workspace.replace(/^\//, "").replace(/\//g, "-");
12826
- return path12.join(
13237
+ return path13.join(
12827
13238
  os5.homedir(),
12828
13239
  ".cursor",
12829
13240
  "projects",
@@ -12859,9 +13270,9 @@ async function runUploadInner(payload) {
12859
13270
  }
12860
13271
  const { repoRoot, config } = match;
12861
13272
  await selfHealHook(repoRoot, sourceTool);
12862
- const transcriptResolved = path12.resolve(transcriptPath);
13273
+ const transcriptResolved = path13.resolve(transcriptPath);
12863
13274
  try {
12864
- const stat = await fs9.promises.stat(transcriptResolved);
13275
+ const stat = await fs10.promises.stat(transcriptResolved);
12865
13276
  if (!stat.isFile()) {
12866
13277
  appendLog(
12867
13278
  "warn",
@@ -12901,13 +13312,13 @@ async function uploadSession(args) {
12901
13312
  };
12902
13313
  const group = {
12903
13314
  repoPath: repoRoot,
12904
- label: path12.basename(repoRoot),
13315
+ label: path13.basename(repoRoot),
12905
13316
  files: [sourceFile],
12906
13317
  sourceNames: [sourceTool],
12907
13318
  lastModified: now
12908
13319
  };
12909
13320
  const envFileNames = await discoverEnvFiles(repoRoot);
12910
- const envFilePaths = envFileNames.map((n) => path12.join(repoRoot, n));
13321
+ const envFilePaths = envFileNames.map((n) => path13.join(repoRoot, n));
12911
13322
  const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
12912
13323
  const mwChain = [];
12913
13324
  if (secretResult.values.size > 0) {
@@ -12932,14 +13343,14 @@ async function uploadSession(args) {
12932
13343
  "copilot-chat": "GitHub Copilot Chat",
12933
13344
  opencode: "opencode"
12934
13345
  };
12935
- const toolLabel = toolLabels[sourceTool] ?? "Claude";
12936
- const epochSeconds = formatEpochSeconds(now);
12937
- 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}`;
12938
13349
  const body = `Session ID: ${sessionId}
12939
- Tool: ${toolLabel}
13350
+ Tool: ${toolLabel2}
12940
13351
  Repo: ${repoRoot}
12941
13352
  Uploaded: ${now.toISOString()}`;
12942
- const zipFilename = `${sourceTool}-${sanitize(shortId)}-${epochSeconds}.zip`;
13353
+ const zipFilename = `${sourceTool}-${sanitize2(shortId)}-${epochSeconds}.zip`;
12943
13354
  const output = new PlatformUploadOutput({
12944
13355
  client,
12945
13356
  projectId: config.projectId,
@@ -13055,7 +13466,6 @@ async function runUploadWorker() {
13055
13466
  appendLog("warn", "worker: invoked with empty stdin; payload expected.");
13056
13467
  return;
13057
13468
  }
13058
- appendLog("info", `worker: raw payload: ${raw.trim()}`);
13059
13469
  let payload;
13060
13470
  try {
13061
13471
  payload = JSON.parse(raw);
@@ -13068,6 +13478,7 @@ async function runUploadWorker() {
13068
13478
  }
13069
13479
  const toolOverride = process.env[TOOL_ENV_FLAG];
13070
13480
  if (toolOverride) payload.tool = toolOverride;
13481
+ appendLog("info", `worker: payload summary: ${summarizePayload(payload)}`);
13071
13482
  try {
13072
13483
  await runUploadInner(payload);
13073
13484
  } catch (err) {
@@ -13075,22 +13486,28 @@ async function runUploadWorker() {
13075
13486
  "error",
13076
13487
  `worker: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
13077
13488
  );
13489
+ } finally {
13490
+ await recordDebugLogCompletion({
13491
+ kind: "agent",
13492
+ tool: resolveSourceTool(payload),
13493
+ payload
13494
+ });
13078
13495
  }
13079
13496
  }
13080
13497
 
13081
13498
  // src/git-traces/index.ts
13082
13499
  import { spawn as spawn3 } from "child_process";
13083
- import crypto2 from "crypto";
13500
+ import crypto3 from "crypto";
13084
13501
 
13085
13502
  // src/git-traces/handlers.ts
13086
13503
  import { execFileSync as execFileSync2 } from "child_process";
13087
- import path15 from "path";
13504
+ import path16 from "path";
13088
13505
 
13089
13506
  // src/git-traces/git-ops.ts
13090
13507
  import { execFileSync } from "child_process";
13091
- import fs10 from "fs";
13508
+ import fs11 from "fs";
13092
13509
  import os6 from "os";
13093
- import path13 from "path";
13510
+ import path14 from "path";
13094
13511
  import { gzipSync } from "zlib";
13095
13512
  var GIT_COMMAND_TIMEOUT_MS = 12e4;
13096
13513
  var EXEC_OPTS = {
@@ -13098,7 +13515,7 @@ var EXEC_OPTS = {
13098
13515
  maxBuffer: 50 * 1024 * 1024
13099
13516
  };
13100
13517
  var MAX_ERROR_OUTPUT_CHARS = 2e3;
13101
- var MAX_UNTRACKED_FILE_BYTES = 10 * 1024 * 1024;
13518
+ var MAX_SNAPSHOT_FILE_BYTES = 10 * 1024 * 1024;
13102
13519
  var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
13103
13520
  function quoteGitArg(arg) {
13104
13521
  if (/^[A-Za-z0-9_./:=@%+,-]+$/.test(arg)) return arg;
@@ -13202,7 +13619,55 @@ function captureSnapshotSha(repoRoot) {
13202
13619
  }
13203
13620
  return git(repoRoot, ["rev-parse", "HEAD"]);
13204
13621
  }
13205
- function buildUntrackedTree(repoRoot) {
13622
+ function recordOmittedSnapshotFile(omittedFiles, file) {
13623
+ omittedFiles?.push(file);
13624
+ const action = file.tracked ? "omitting tracked" : "skipping untracked";
13625
+ appendLog(
13626
+ "warn",
13627
+ `git-traces: ${action} ${file.path} (${file.sizeBytes} bytes > ${MAX_SNAPSHOT_FILE_BYTES} limit)`
13628
+ );
13629
+ }
13630
+ function parseLsTreeLongZ(output) {
13631
+ const entries = [];
13632
+ for (const record of output.toString("utf-8").split("\0")) {
13633
+ if (!record) continue;
13634
+ const tab = record.indexOf(" ");
13635
+ if (tab === -1) continue;
13636
+ const meta = record.slice(0, tab);
13637
+ const filePath = record.slice(tab + 1);
13638
+ const [mode, type, sha, sizeRaw] = meta.trim().split(/\s+/);
13639
+ if (type !== "blob" || !mode || !sha || !sizeRaw) continue;
13640
+ const sizeBytes = Number.parseInt(sizeRaw, 10);
13641
+ if (!Number.isFinite(sizeBytes)) continue;
13642
+ entries.push({ mode, sha, sizeBytes, path: filePath });
13643
+ }
13644
+ return entries;
13645
+ }
13646
+ function listOversizedTreeFiles(repoRoot, treeSha, omittedFiles) {
13647
+ const output = gitBuffer(repoRoot, ["ls-tree", "-r", "-l", "-z", treeSha]);
13648
+ const oversized = [];
13649
+ for (const entry of parseLsTreeLongZ(output)) {
13650
+ if (entry.sizeBytes <= MAX_SNAPSHOT_FILE_BYTES) continue;
13651
+ const file = {
13652
+ path: entry.path,
13653
+ sizeBytes: entry.sizeBytes,
13654
+ tracked: true,
13655
+ reason: "file-over-limit",
13656
+ gitObjectSha: entry.sha
13657
+ };
13658
+ oversized.push(file);
13659
+ recordOmittedSnapshotFile(omittedFiles, file);
13660
+ }
13661
+ return oversized;
13662
+ }
13663
+ function removePathsFromIndex(repoRoot, env, paths) {
13664
+ if (paths.length === 0) return;
13665
+ gitBuffer(repoRoot, ["update-index", "--force-remove", "-z", "--stdin"], {
13666
+ input: `${paths.join("\0")}\0`,
13667
+ env
13668
+ });
13669
+ }
13670
+ function buildUntrackedTree(repoRoot, omittedFiles) {
13206
13671
  const list = git(repoRoot, [
13207
13672
  "ls-files",
13208
13673
  "--others",
@@ -13214,12 +13679,14 @@ function buildUntrackedTree(repoRoot) {
13214
13679
  for (const relPath of list.split("\0")) {
13215
13680
  if (!relPath) continue;
13216
13681
  try {
13217
- const stat = fs10.lstatSync(path13.join(repoRoot, relPath));
13218
- if (stat.size > MAX_UNTRACKED_FILE_BYTES) {
13219
- appendLog(
13220
- "warn",
13221
- `git-traces: skipping untracked ${relPath} (${stat.size} bytes > ${MAX_UNTRACKED_FILE_BYTES} limit)`
13222
- );
13682
+ const stat = fs11.lstatSync(path14.join(repoRoot, relPath));
13683
+ if (stat.size > MAX_SNAPSHOT_FILE_BYTES) {
13684
+ recordOmittedSnapshotFile(omittedFiles, {
13685
+ path: relPath,
13686
+ sizeBytes: stat.size,
13687
+ tracked: false,
13688
+ reason: "file-over-limit"
13689
+ });
13223
13690
  continue;
13224
13691
  }
13225
13692
  kept.push(relPath);
@@ -13227,7 +13694,7 @@ function buildUntrackedTree(repoRoot) {
13227
13694
  }
13228
13695
  }
13229
13696
  if (kept.length === 0) return null;
13230
- const tmpIndex = path13.join(
13697
+ const tmpIndex = path14.join(
13231
13698
  os6.tmpdir(),
13232
13699
  `hillclimb-untracked-${Date.now()}-${process.pid}`
13233
13700
  );
@@ -13240,46 +13707,58 @@ function buildUntrackedTree(repoRoot) {
13240
13707
  return gitWithEnv(repoRoot, ["write-tree"], env);
13241
13708
  } finally {
13242
13709
  try {
13243
- fs10.unlinkSync(tmpIndex);
13710
+ fs11.unlinkSync(tmpIndex);
13244
13711
  } catch {
13245
13712
  }
13246
13713
  }
13247
13714
  }
13248
- function buildSnapshotTree(repoRoot, stashSha) {
13715
+ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
13249
13716
  const trackedTree = git(repoRoot, ["rev-parse", `${stashSha}^{tree}`]);
13250
- const untrackedTree = buildUntrackedTree(repoRoot);
13251
- if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA) {
13717
+ const oversizedTracked = listOversizedTreeFiles(
13718
+ repoRoot,
13719
+ trackedTree,
13720
+ options.omittedFiles
13721
+ );
13722
+ const untrackedTree = buildUntrackedTree(repoRoot, options.omittedFiles);
13723
+ if (oversizedTracked.length === 0 && (!untrackedTree || untrackedTree === EMPTY_TREE_SHA)) {
13252
13724
  return trackedTree;
13253
13725
  }
13254
- const tmpIndex = path13.join(
13726
+ const tmpIndex = path14.join(
13255
13727
  os6.tmpdir(),
13256
13728
  `hillclimb-index-${Date.now()}-${process.pid}`
13257
13729
  );
13258
13730
  const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
13259
13731
  try {
13260
13732
  gitWithEnv(repoRoot, ["read-tree", trackedTree], env);
13261
- const untrackedList = gitBuffer(
13733
+ removePathsFromIndex(
13262
13734
  repoRoot,
13263
- ["ls-tree", "-r", untrackedTree],
13264
- {
13265
- env
13735
+ env,
13736
+ oversizedTracked.map((file) => file.path)
13737
+ );
13738
+ if (untrackedTree && untrackedTree !== EMPTY_TREE_SHA) {
13739
+ const untrackedList = gitBuffer(
13740
+ repoRoot,
13741
+ ["ls-tree", "-r", untrackedTree],
13742
+ {
13743
+ env
13744
+ }
13745
+ ).toString("utf-8");
13746
+ const indexInfo = untrackedList.split("\n").filter(Boolean).map((line) => {
13747
+ const [meta, filePath] = line.split(" ");
13748
+ const [mode, , sha] = meta.split(/\s+/);
13749
+ return `${mode} ${sha} ${filePath}`;
13750
+ }).join("\n");
13751
+ if (indexInfo) {
13752
+ gitBuffer(repoRoot, ["update-index", "--index-info"], {
13753
+ input: indexInfo,
13754
+ env
13755
+ });
13266
13756
  }
13267
- ).toString("utf-8");
13268
- const indexInfo = untrackedList.split("\n").filter(Boolean).map((line) => {
13269
- const [meta, filePath] = line.split(" ");
13270
- const [mode, , sha] = meta.split(/\s+/);
13271
- return `${mode} ${sha} ${filePath}`;
13272
- }).join("\n");
13273
- if (indexInfo) {
13274
- gitBuffer(repoRoot, ["update-index", "--index-info"], {
13275
- input: indexInfo,
13276
- env
13277
- });
13278
13757
  }
13279
13758
  return gitWithEnv(repoRoot, ["write-tree"], env);
13280
13759
  } finally {
13281
13760
  try {
13282
- fs10.unlinkSync(tmpIndex);
13761
+ fs11.unlinkSync(tmpIndex);
13283
13762
  } catch {
13284
13763
  }
13285
13764
  }
@@ -13293,16 +13772,16 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
13293
13772
  ]);
13294
13773
  const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
13295
13774
  pinRef(repoRoot, orphanRef, orphanCommit);
13296
- const tmpFile = path13.join(
13775
+ const tmpFile = path14.join(
13297
13776
  os6.tmpdir(),
13298
13777
  `hillclimb-bundle-${Date.now()}.bundle`
13299
13778
  );
13300
13779
  try {
13301
13780
  git(repoRoot, ["bundle", "create", tmpFile, orphanRef]);
13302
- return fs10.readFileSync(tmpFile);
13781
+ return fs11.readFileSync(tmpFile);
13303
13782
  } finally {
13304
13783
  try {
13305
- fs10.unlinkSync(tmpFile);
13784
+ fs11.unlinkSync(tmpFile);
13306
13785
  } catch {
13307
13786
  }
13308
13787
  deleteRef(repoRoot, orphanRef);
@@ -13361,7 +13840,7 @@ function parseDirtyFilesFromStatus(status) {
13361
13840
  return pathPart;
13362
13841
  }).filter(Boolean);
13363
13842
  }
13364
- function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind, startedAt) {
13843
+ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind, startedAt, omittedFiles = []) {
13365
13844
  const headSha = safeGit(repoRoot, ["rev-parse", "HEAD"]) ?? "unknown";
13366
13845
  const branch = safeGit(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? null;
13367
13846
  const remoteUrl = safeGit(repoRoot, ["config", "--get", "remote.origin.url"]) ?? null;
@@ -13385,7 +13864,8 @@ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersio
13385
13864
  branch,
13386
13865
  remoteUrl,
13387
13866
  isDirty: dirtyFiles.length > 0,
13388
- dirtyFiles
13867
+ dirtyFiles,
13868
+ ...omittedFiles.length > 0 ? { omittedFiles } : {}
13389
13869
  },
13390
13870
  author: { name: authorName, email: authorEmail },
13391
13871
  hostname: os6.hostname(),
@@ -13477,9 +13957,9 @@ function parseCommitFiles(repoRoot, sha) {
13477
13957
  oldPath
13478
13958
  });
13479
13959
  } else {
13480
- const path22 = parts[parts.length - 1];
13481
- indexByPath.set(path22, files.length);
13482
- files.push({ path: path22, status, additions: 0, deletions: 0 });
13960
+ const path23 = parts[parts.length - 1];
13961
+ indexByPath.set(path23, files.length);
13962
+ files.push({ path: path23, status, additions: 0, deletions: 0 });
13483
13963
  }
13484
13964
  }
13485
13965
  for (const line of numstat.split("\n")) {
@@ -13545,31 +14025,31 @@ function cleanupSessionRefs(repoRoot, sessionId) {
13545
14025
  }
13546
14026
 
13547
14027
  // src/git-traces/session-state.ts
13548
- import crypto from "crypto";
13549
- import fs11 from "fs";
14028
+ import crypto2 from "crypto";
14029
+ import fs12 from "fs";
13550
14030
  import os7 from "os";
13551
- import path14 from "path";
13552
- var CURRENT_SCHEMA_VERSION = 3;
13553
- var DEFAULT_STATE_DIR = path14.join(os7.homedir(), ".hillclimb", "git-traces");
13554
- var LOCK_RETRIES = 120;
13555
- var LOCK_RETRY_DELAY_MS = 500;
13556
- function stateDir() {
14031
+ import path15 from "path";
14032
+ var CURRENT_SCHEMA_VERSION2 = 3;
14033
+ var DEFAULT_STATE_DIR = path15.join(os7.homedir(), ".hillclimb", "git-traces");
14034
+ var LOCK_RETRIES2 = 120;
14035
+ var LOCK_RETRY_DELAY_MS2 = 500;
14036
+ function stateDir2() {
13557
14037
  return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR;
13558
14038
  }
13559
14039
  function stateFileForRepo(repoRoot, tool, sessionId) {
13560
- const hash = crypto.createHash("sha256").update(
13561
- sessionId ? `${path14.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path14.resolve(repoRoot)}\0${tool}`
14040
+ const hash = crypto2.createHash("sha256").update(
14041
+ sessionId ? `${path15.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path15.resolve(repoRoot)}\0${tool}`
13562
14042
  ).digest("hex").slice(0, 16);
13563
- return path14.join(stateDir(), `${hash}.json`);
14043
+ return path15.join(stateDir2(), `${hash}.json`);
13564
14044
  }
13565
14045
  function lockFileForRepo(repoRoot, tool) {
13566
14046
  return `${stateFileForRepo(repoRoot, tool)}.lock`;
13567
14047
  }
13568
14048
  async function readStateFile(file) {
13569
14049
  try {
13570
- const raw = await fs11.promises.readFile(file, "utf-8");
14050
+ const raw = await fs12.promises.readFile(file, "utf-8");
13571
14051
  const parsed = JSON.parse(raw);
13572
- if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) {
14052
+ if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION2) {
13573
14053
  return null;
13574
14054
  }
13575
14055
  return parsed;
@@ -13580,26 +14060,26 @@ async function readStateFile(file) {
13580
14060
  async function listScopedSessionStates(repoRoot, tool) {
13581
14061
  let entries;
13582
14062
  try {
13583
- entries = await fs11.promises.readdir(stateDir(), { withFileTypes: true });
14063
+ entries = await fs12.promises.readdir(stateDir2(), { withFileTypes: true });
13584
14064
  } catch {
13585
14065
  return [];
13586
14066
  }
13587
14067
  const states = [];
13588
14068
  for (const entry of entries) {
13589
14069
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
13590
- const file = path14.join(stateDir(), entry.name);
14070
+ const file = path15.join(stateDir2(), entry.name);
13591
14071
  const state = await readStateFile(file);
13592
14072
  if (!state) continue;
13593
14073
  if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
13594
14074
  continue;
13595
14075
  }
13596
- if (path14.resolve(state.repoRoot) !== path14.resolve(repoRoot)) continue;
13597
- if (path14.resolve(file) !== path14.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
14076
+ if (path15.resolve(state.repoRoot) !== path15.resolve(repoRoot)) continue;
14077
+ if (path15.resolve(file) !== path15.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
13598
14078
  continue;
13599
14079
  }
13600
14080
  let mtimeMs = 0;
13601
14081
  try {
13602
- mtimeMs = (await fs11.promises.stat(file)).mtimeMs;
14082
+ mtimeMs = (await fs12.promises.stat(file)).mtimeMs;
13603
14083
  } catch {
13604
14084
  continue;
13605
14085
  }
@@ -13610,26 +14090,26 @@ async function listScopedSessionStates(repoRoot, tool) {
13610
14090
  async function listSessionStatesForSession(tool, sessionId) {
13611
14091
  let entries;
13612
14092
  try {
13613
- entries = await fs11.promises.readdir(stateDir(), { withFileTypes: true });
14093
+ entries = await fs12.promises.readdir(stateDir2(), { withFileTypes: true });
13614
14094
  } catch {
13615
14095
  return [];
13616
14096
  }
13617
14097
  const states = [];
13618
14098
  for (const entry of entries) {
13619
14099
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
13620
- const file = path14.join(stateDir(), entry.name);
14100
+ const file = path15.join(stateDir2(), entry.name);
13621
14101
  const state = await readStateFile(file);
13622
14102
  if (!state) continue;
13623
14103
  if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
13624
14104
  continue;
13625
14105
  }
13626
14106
  if (state.sessionId !== sessionId) continue;
13627
- if (path14.resolve(file) !== path14.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
14107
+ if (path15.resolve(file) !== path15.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
13628
14108
  continue;
13629
14109
  }
13630
14110
  let mtimeMs = 0;
13631
14111
  try {
13632
- mtimeMs = (await fs11.promises.stat(file)).mtimeMs;
14112
+ mtimeMs = (await fs12.promises.stat(file)).mtimeMs;
13633
14113
  } catch {
13634
14114
  continue;
13635
14115
  }
@@ -13650,12 +14130,12 @@ async function readSessionState(repoRoot, tool, sessionId) {
13650
14130
  }
13651
14131
  async function writeSessionState(state, tool) {
13652
14132
  const file = stateFileForRepo(state.repoRoot, tool, state.sessionId);
13653
- await fs11.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
14133
+ await fs12.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
13654
14134
  const tmp = `${file}.tmp`;
13655
- await fs11.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
14135
+ await fs12.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
13656
14136
  mode: 384
13657
14137
  });
13658
- await fs11.promises.rename(tmp, file);
14138
+ await fs12.promises.rename(tmp, file);
13659
14139
  const legacyFile = stateFileForRepo(state.repoRoot, tool);
13660
14140
  const legacy = await readStateFile(legacyFile);
13661
14141
  if (legacy?.sessionId === state.sessionId) {
@@ -13664,7 +14144,7 @@ async function writeSessionState(state, tool) {
13664
14144
  }
13665
14145
  async function deleteStateFile(file) {
13666
14146
  try {
13667
- await fs11.promises.unlink(file);
14147
+ await fs12.promises.unlink(file);
13668
14148
  } catch {
13669
14149
  }
13670
14150
  }
@@ -13680,14 +14160,14 @@ async function deleteSessionState(repoRoot, tool, sessionId) {
13680
14160
  }
13681
14161
  await deleteStateFile(stateFileForRepo(repoRoot, tool));
13682
14162
  }
13683
- async function acquireLock(repoRoot, tool, retries = LOCK_RETRIES, delayMs = LOCK_RETRY_DELAY_MS) {
14163
+ async function acquireLock2(repoRoot, tool, retries = LOCK_RETRIES2, delayMs = LOCK_RETRY_DELAY_MS2) {
13684
14164
  const lockPath = lockFileForRepo(repoRoot, tool);
13685
- await fs11.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
14165
+ await fs12.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
13686
14166
  for (let i = 0; i < retries; i++) {
13687
14167
  try {
13688
- const fd = await fs11.promises.open(
14168
+ const fd = await fs12.promises.open(
13689
14169
  lockPath,
13690
- fs11.constants.O_CREAT | fs11.constants.O_EXCL | fs11.constants.O_WRONLY
14170
+ fs12.constants.O_CREAT | fs12.constants.O_EXCL | fs12.constants.O_WRONLY
13691
14171
  );
13692
14172
  await fd.write(String(process.pid));
13693
14173
  await fd.close();
@@ -13702,9 +14182,9 @@ async function acquireLock(repoRoot, tool, retries = LOCK_RETRIES, delayMs = LOC
13702
14182
  }
13703
14183
  throw new Error(`Failed to acquire lock after ${retries} retries`);
13704
14184
  }
13705
- async function releaseLock(repoRoot, tool) {
14185
+ async function releaseLock2(repoRoot, tool) {
13706
14186
  try {
13707
- await fs11.promises.unlink(lockFileForRepo(repoRoot, tool));
14187
+ await fs12.promises.unlink(lockFileForRepo(repoRoot, tool));
13708
14188
  } catch {
13709
14189
  }
13710
14190
  }
@@ -13714,7 +14194,7 @@ var CLI_VERSION = "0.2.0";
13714
14194
  var GIT_TRACES_SLUG = "git-traces";
13715
14195
  var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
13716
14196
  var STALE_SCOPED_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
13717
- function formatEpochSeconds2(date) {
14197
+ function formatEpochSeconds3(date) {
13718
14198
  return String(Math.floor(date.getTime() / 1e3));
13719
14199
  }
13720
14200
  var TOOL_LABELS = {
@@ -13727,17 +14207,17 @@ var TOOL_LABELS = {
13727
14207
  async function loadConfiguredRepos() {
13728
14208
  const file = await loadProjects();
13729
14209
  return Object.entries(file.projects).map(([repoRoot, config]) => ({
13730
- repoRoot: path15.resolve(repoRoot),
14210
+ repoRoot: path16.resolve(repoRoot),
13731
14211
  config
13732
14212
  })).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
13733
14213
  }
13734
14214
  function repoLabel(repoRoot) {
13735
- return path15.basename(repoRoot) || repoRoot;
14215
+ return path16.basename(repoRoot) || repoRoot;
13736
14216
  }
13737
- function resolveCwd(payload) {
14217
+ function resolveCwd2(payload) {
13738
14218
  return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
13739
14219
  }
13740
- function resolveSessionId(payload) {
14220
+ function resolveSessionId2(payload) {
13741
14221
  return payload.session_id ?? payload.conversation_id ?? null;
13742
14222
  }
13743
14223
  function epochPrefix(epoch) {
@@ -13829,6 +14309,10 @@ function freezeEpochBaseline(params) {
13829
14309
  sessionId,
13830
14310
  epoch
13831
14311
  );
14312
+ const omittedFiles = [];
14313
+ const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha, {
14314
+ omittedFiles
14315
+ });
13832
14316
  const baselineMetadata = buildBaselineMetadata(
13833
14317
  repoRoot,
13834
14318
  sessionId,
@@ -13838,9 +14322,9 @@ function freezeEpochBaseline(params) {
13838
14322
  epoch,
13839
14323
  prevHeadSha,
13840
14324
  transitionKind,
13841
- startedAt
14325
+ startedAt,
14326
+ omittedFiles
13842
14327
  );
13843
- const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha);
13844
14328
  pinFrozenBaselineTree(repoRoot, sessionId, epoch, baselineTreeSha);
13845
14329
  return { baselineSha, baselineTreeSha, baselineMetadata, headSha };
13846
14330
  } catch (err) {
@@ -13886,6 +14370,10 @@ function buildEpochBaselineArtifacts(params) {
13886
14370
  } = params;
13887
14371
  const prefix = epochPrefix(epoch);
13888
14372
  try {
14373
+ const omittedFiles = [];
14374
+ const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha, {
14375
+ omittedFiles
14376
+ });
13889
14377
  const metadata = buildBaselineMetadata(
13890
14378
  repoRoot,
13891
14379
  sessionId,
@@ -13895,9 +14383,9 @@ function buildEpochBaselineArtifacts(params) {
13895
14383
  epoch,
13896
14384
  prevHeadSha,
13897
14385
  transitionKind,
13898
- startedAt
14386
+ startedAt,
14387
+ omittedFiles
13899
14388
  );
13900
- const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha);
13901
14389
  return buildFrozenEpochBaselineArtifacts({
13902
14390
  repoRoot,
13903
14391
  sessionId,
@@ -13935,15 +14423,15 @@ async function uploadEpochBaselineArtifacts(params) {
13935
14423
  }
13936
14424
  async function createGitTracesContribution(params) {
13937
14425
  const { client, config, repoRoot, state, tool, now, artifacts } = params;
13938
- const toolLabel = TOOL_LABELS[tool] ?? "Claude";
13939
- const epochSeconds = formatEpochSeconds2(now);
14426
+ const toolLabel2 = TOOL_LABELS[tool] ?? "Claude";
14427
+ const epochSeconds = formatEpochSeconds3(now);
13940
14428
  const shortId = state.sessionId.slice(0, 12);
13941
14429
  const repoName = repoLabel(repoRoot);
13942
14430
  const contribution = await client.createContribution(config.projectId, {
13943
14431
  contributionTypeSlug: GIT_TRACES_SLUG,
13944
- title: `${toolLabel} session ${shortId} \u2014 ${repoName} \u2014 ${epochSeconds}`,
14432
+ title: `${toolLabel2} session ${shortId} \u2014 ${repoName} \u2014 ${epochSeconds}`,
13945
14433
  body: `Session ID: ${state.sessionId}
13946
- Tool: ${toolLabel}
14434
+ Tool: ${toolLabel2}
13947
14435
  Repo: ${repoRoot}
13948
14436
  Uploaded: ${now.toISOString()}`
13949
14437
  });
@@ -13997,7 +14485,7 @@ async function initializeSession(repoRoot, tool, sessionId) {
13997
14485
  });
13998
14486
  if (!frozen) return null;
13999
14487
  const state = {
14000
- schemaVersion: CURRENT_SCHEMA_VERSION,
14488
+ schemaVersion: CURRENT_SCHEMA_VERSION2,
14001
14489
  sessionId,
14002
14490
  contributionId: null,
14003
14491
  baselineSha: frozen.baselineSha,
@@ -14047,7 +14535,7 @@ async function processSessionStartRepo(repo, tool, sessionId) {
14047
14535
  );
14048
14536
  return "skipped";
14049
14537
  }
14050
- await acquireLock(repoRoot, tool);
14538
+ await acquireLock2(repoRoot, tool);
14051
14539
  try {
14052
14540
  const staleLegacy = await readSessionState(repoRoot, tool);
14053
14541
  if (staleLegacy && staleLegacy.sessionId !== sessionId) {
@@ -14078,11 +14566,11 @@ async function processSessionStartRepo(repo, tool, sessionId) {
14078
14566
  );
14079
14567
  return "failed";
14080
14568
  } finally {
14081
- await releaseLock(repoRoot, tool);
14569
+ await releaseLock2(repoRoot, tool);
14082
14570
  }
14083
14571
  }
14084
14572
  async function handleSessionStart(payload, tool) {
14085
- const cwd = resolveCwd(payload);
14573
+ const cwd = resolveCwd2(payload);
14086
14574
  if (!cwd) {
14087
14575
  appendLog("warn", "git-traces: no cwd in payload, skipping");
14088
14576
  return;
@@ -14095,7 +14583,7 @@ async function handleSessionStart(payload, tool) {
14095
14583
  );
14096
14584
  return;
14097
14585
  }
14098
- const sessionId = resolveSessionId(payload);
14586
+ const sessionId = resolveSessionId2(payload);
14099
14587
  if (!sessionId) {
14100
14588
  appendLog("warn", "git-traces: no session_id in payload, skipping");
14101
14589
  return;
@@ -14178,9 +14666,10 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
14178
14666
  );
14179
14667
  return "skipped";
14180
14668
  }
14181
- await acquireLock(repoRoot, tool);
14669
+ await acquireLock2(repoRoot, tool);
14670
+ let state = null;
14182
14671
  try {
14183
- const state = await readSessionState(repoRoot, tool, sessionId);
14672
+ state = await readSessionState(repoRoot, tool, sessionId);
14184
14673
  if (!state) {
14185
14674
  appendLog(
14186
14675
  "info",
@@ -14351,17 +14840,42 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
14351
14840
  "error",
14352
14841
  `git-traces: Stop failed for repo ${repoRoot}: ${formatError(err)}`
14353
14842
  );
14843
+ if (state && err instanceof PlatformError && err.status === 404 && err.code === "CONTRIBUTION_NOT_FOUND") {
14844
+ if (state.baselineTreeSha) {
14845
+ await writeSessionState(
14846
+ {
14847
+ ...state,
14848
+ contributionId: null,
14849
+ lastSnapshotSha: state.baselineSha,
14850
+ lastSnapshotTreeSha: state.baselineTreeSha,
14851
+ turnCount: 0
14852
+ },
14853
+ tool
14854
+ );
14855
+ appendLog(
14856
+ "warn",
14857
+ `git-traces: cleared stale contribution state (repo=${repoRoot}, project=${config.projectId}, missingContribution=${state.contributionId})`
14858
+ );
14859
+ } else {
14860
+ cleanupSessionRefs(repoRoot, state.sessionId);
14861
+ await deleteSessionState(repoRoot, tool, state.sessionId);
14862
+ appendLog(
14863
+ "warn",
14864
+ `git-traces: deleted stale contribution state without a frozen baseline (repo=${repoRoot}, project=${config.projectId}, missingContribution=${state.contributionId})`
14865
+ );
14866
+ }
14867
+ }
14354
14868
  return "failed";
14355
14869
  } finally {
14356
- await releaseLock(repoRoot, tool);
14870
+ await releaseLock2(repoRoot, tool);
14357
14871
  }
14358
14872
  }
14359
14873
  async function handleStop(payload, tool) {
14360
- const cwd = resolveCwd(payload);
14874
+ const cwd = resolveCwd2(payload);
14361
14875
  if (!cwd) return;
14362
14876
  const project = await findProjectForCwd(cwd);
14363
14877
  if (!project) return;
14364
- const sessionId = resolveSessionId(payload);
14878
+ const sessionId = resolveSessionId2(payload);
14365
14879
  const recordedAt = Date.now();
14366
14880
  const repos = await loadConfiguredRepos();
14367
14881
  const repoByRoot = new Map(repos.map((repo) => [repo.repoRoot, repo]));
@@ -14370,7 +14884,7 @@ async function handleStop(payload, tool) {
14370
14884
  if (sessionId) {
14371
14885
  const storedStates = await listSessionStatesForSession(tool, sessionId);
14372
14886
  for (const { state } of storedStates) {
14373
- const repo = repoByRoot.get(path15.resolve(state.repoRoot));
14887
+ const repo = repoByRoot.get(path16.resolve(state.repoRoot));
14374
14888
  if (!repo) {
14375
14889
  missingConfig++;
14376
14890
  appendLog(
@@ -14406,7 +14920,7 @@ async function handleStop(payload, tool) {
14406
14920
  );
14407
14921
  }
14408
14922
  async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
14409
- await acquireLock(repoRoot, tool);
14923
+ await acquireLock2(repoRoot, tool);
14410
14924
  try {
14411
14925
  const state = await readSessionState(repoRoot, tool, sessionId);
14412
14926
  if (!state) return "no-state";
@@ -14424,19 +14938,19 @@ async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
14424
14938
  );
14425
14939
  return "failed";
14426
14940
  } finally {
14427
- await releaseLock(repoRoot, tool);
14941
+ await releaseLock2(repoRoot, tool);
14428
14942
  }
14429
14943
  }
14430
14944
  async function handleSessionEnd(payload, tool) {
14431
- const cwd = resolveCwd(payload);
14432
- const sessionId = resolveSessionId(payload);
14945
+ const cwd = resolveCwd2(payload);
14946
+ const sessionId = resolveSessionId2(payload);
14433
14947
  const project = cwd ? await findProjectForCwd(cwd) : null;
14434
14948
  const triggerRepo = project?.repoRoot ?? cwd ?? "<none>";
14435
14949
  const repoRoots = [];
14436
14950
  if (sessionId) {
14437
14951
  const states = await listSessionStatesForSession(tool, sessionId);
14438
14952
  for (const { state } of states) {
14439
- repoRoots.push(path15.resolve(state.repoRoot));
14953
+ repoRoots.push(path16.resolve(state.repoRoot));
14440
14954
  }
14441
14955
  }
14442
14956
  if (repoRoots.length === 0 && cwd) {
@@ -14463,7 +14977,7 @@ var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
14463
14977
  var TOOL_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_TOOL";
14464
14978
  var FLOW_ID_ENV = "HILLCLIMB_GIT_TRACES_FLOW";
14465
14979
  function newFlowId() {
14466
- return crypto2.randomBytes(3).toString("hex");
14980
+ return crypto3.randomBytes(3).toString("hex");
14467
14981
  }
14468
14982
  var KNOWN_TOOLS = /* @__PURE__ */ new Set([
14469
14983
  "claude",
@@ -14472,7 +14986,7 @@ var KNOWN_TOOLS = /* @__PURE__ */ new Set([
14472
14986
  "cursor",
14473
14987
  "opencode"
14474
14988
  ]);
14475
- function classifyHookEvent(event) {
14989
+ function classifyHookEvent2(event) {
14476
14990
  switch (event) {
14477
14991
  case "SessionStart":
14478
14992
  case "sessionStart":
@@ -14507,7 +15021,7 @@ async function readStdin2() {
14507
15021
  }
14508
15022
  return Buffer.concat(chunks).toString("utf-8");
14509
15023
  }
14510
- function resolveCwd2(payload) {
15024
+ function resolveCwd3(payload) {
14511
15025
  return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
14512
15026
  }
14513
15027
  async function repairHookForTool(repoRoot, tool) {
@@ -14522,7 +15036,7 @@ async function repairHookForTool(repoRoot, tool) {
14522
15036
  }
14523
15037
  async function selfHealHook2(payload, tool) {
14524
15038
  if (process.env.HILLCLIMB_SKIP_HOOK_SELF_HEAL === "1") return;
14525
- const cwd = resolveCwd2(payload);
15039
+ const cwd = resolveCwd3(payload);
14526
15040
  if (!cwd) return;
14527
15041
  try {
14528
15042
  const project = await findProjectForCwd(cwd);
@@ -14546,7 +15060,7 @@ async function resolveLegacyBareTool(raw) {
14546
15060
  );
14547
15061
  return null;
14548
15062
  }
14549
- const cwd = resolveCwd2(payload);
15063
+ const cwd = resolveCwd3(payload);
14550
15064
  if (!cwd) {
14551
15065
  appendLog(
14552
15066
  "error",
@@ -14705,6 +15219,7 @@ async function runGitTracesWorker() {
14705
15219
  return;
14706
15220
  }
14707
15221
  const event = payload.hook_event_name;
15222
+ const eventKind = classifyHookEvent2(event);
14708
15223
  appendLog("info", `git-traces worker: handling event=${event}`);
14709
15224
  if (tool === "claude" && typeof payload.cursor_version === "string") {
14710
15225
  appendLog(
@@ -14713,9 +15228,9 @@ async function runGitTracesWorker() {
14713
15228
  );
14714
15229
  return;
14715
15230
  }
14716
- await selfHealHook2(payload, tool);
14717
15231
  try {
14718
- switch (classifyHookEvent(event)) {
15232
+ await selfHealHook2(payload, tool);
15233
+ switch (eventKind) {
14719
15234
  case "sessionStart":
14720
15235
  await handleSessionStart(payload, tool);
14721
15236
  break;
@@ -14736,19 +15251,27 @@ async function runGitTracesWorker() {
14736
15251
  `git-traces worker: unexpected error: ${detail}${stack ? `
14737
15252
  ${stack}` : ""}`
14738
15253
  );
15254
+ } finally {
15255
+ if (eventKind === "stop" || eventKind === "sessionEnd") {
15256
+ await recordDebugLogCompletion({
15257
+ kind: "git",
15258
+ tool,
15259
+ payload
15260
+ });
15261
+ }
14739
15262
  }
14740
15263
  }
14741
15264
 
14742
15265
  // src/outputs/zip.ts
14743
- import fs13 from "fs";
14744
- import path17 from "path";
15266
+ import fs14 from "fs";
15267
+ import path18 from "path";
14745
15268
  import archiver2 from "archiver";
14746
15269
 
14747
15270
  // src/outputs/downloads.ts
14748
15271
  import { execSync as execSync2 } from "child_process";
14749
- import fs12 from "fs";
15272
+ import fs13 from "fs";
14750
15273
  import os8 from "os";
14751
- import path16 from "path";
15274
+ import path17 from "path";
14752
15275
  function getDownloadsFolder() {
14753
15276
  const home = os8.homedir();
14754
15277
  if (process.platform === "linux") {
@@ -14757,12 +15280,12 @@ function getDownloadsFolder() {
14757
15280
  encoding: "utf-8",
14758
15281
  timeout: 3e3
14759
15282
  }).trim();
14760
- if (xdgDir && fs12.existsSync(xdgDir)) return xdgDir;
15283
+ if (xdgDir && fs13.existsSync(xdgDir)) return xdgDir;
14761
15284
  } catch {
14762
15285
  }
14763
15286
  }
14764
- const downloads = path16.join(home, "Downloads");
14765
- if (fs12.existsSync(downloads)) return downloads;
15287
+ const downloads = path17.join(home, "Downloads");
15288
+ if (fs13.existsSync(downloads)) return downloads;
14766
15289
  return home;
14767
15290
  }
14768
15291
 
@@ -14771,11 +15294,11 @@ function sanitizeFilename(name) {
14771
15294
  return name.replace(/[^a-zA-Z0-9._-]/g, "_");
14772
15295
  }
14773
15296
  function getUniqueFilename(dir, base, ext) {
14774
- let candidate = path17.join(dir, `${base}${ext}`);
14775
- if (!fs13.existsSync(candidate)) return candidate;
15297
+ let candidate = path18.join(dir, `${base}${ext}`);
15298
+ if (!fs14.existsSync(candidate)) return candidate;
14776
15299
  let i = 1;
14777
- while (fs13.existsSync(candidate)) {
14778
- candidate = path17.join(dir, `${base}-${i}${ext}`);
15300
+ while (fs14.existsSync(candidate)) {
15301
+ candidate = path18.join(dir, `${base}-${i}${ext}`);
14779
15302
  i++;
14780
15303
  }
14781
15304
  return candidate;
@@ -14785,13 +15308,13 @@ var ZipOutput = class {
14785
15308
  label = "Save as .zip to Downloads";
14786
15309
  async emit(group, options) {
14787
15310
  const downloadsDir = getDownloadsFolder();
14788
- const repoName = sanitizeFilename(path17.basename(group.repoPath));
15311
+ const repoName = sanitizeFilename(path18.basename(group.repoPath));
14789
15312
  const timeRange = options.timeRange;
14790
15313
  const rangePart = timeRange?.label ?? "all";
14791
15314
  const epochSeconds = Math.floor(Date.now() / 1e3);
14792
15315
  const baseName = `logs-${repoName}-${rangePart}-${epochSeconds}`;
14793
15316
  const outputPath = getUniqueFilename(downloadsDir, baseName, ".zip");
14794
- const output = fs13.createWriteStream(outputPath);
15317
+ const output = fs14.createWriteStream(outputPath);
14795
15318
  const archive = archiver2("zip", { zlib: { level: 6 } });
14796
15319
  const done = new Promise((resolve, reject) => {
14797
15320
  output.on("close", resolve);
@@ -14985,15 +15508,15 @@ async function confirmExport(group, output) {
14985
15508
  }
14986
15509
 
14987
15510
  // src/sources/claude.ts
14988
- import fs14 from "fs";
15511
+ import fs15 from "fs";
14989
15512
  import os9 from "os";
14990
- import path18 from "path";
15513
+ import path19 from "path";
14991
15514
  import readline from "readline";
14992
15515
  var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
14993
15516
  async function resolveRepoPath(projectDir) {
14994
- const indexPath = path18.join(projectDir, "sessions-index.json");
15517
+ const indexPath = path19.join(projectDir, "sessions-index.json");
14995
15518
  try {
14996
- const raw = await fs14.promises.readFile(indexPath, "utf-8");
15519
+ const raw = await fs15.promises.readFile(indexPath, "utf-8");
14997
15520
  const data = JSON.parse(raw);
14998
15521
  if (data.originalPath && typeof data.originalPath === "string") {
14999
15522
  return data.originalPath;
@@ -15001,12 +15524,12 @@ async function resolveRepoPath(projectDir) {
15001
15524
  } catch {
15002
15525
  }
15003
15526
  const cwdCounts = /* @__PURE__ */ new Map();
15004
- const entries = await fs14.promises.readdir(projectDir, {
15527
+ const entries = await fs15.promises.readdir(projectDir, {
15005
15528
  withFileTypes: true
15006
15529
  });
15007
15530
  for (const entry of entries) {
15008
15531
  if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
15009
- const cwd = await extractCwdFromJsonl(path18.join(projectDir, entry.name));
15532
+ const cwd = await extractCwdFromJsonl(path19.join(projectDir, entry.name));
15010
15533
  if (cwd) {
15011
15534
  cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
15012
15535
  }
@@ -15025,7 +15548,7 @@ async function resolveRepoPath(projectDir) {
15025
15548
  return null;
15026
15549
  }
15027
15550
  async function extractCwdFromJsonl(filePath) {
15028
- const stream = fs14.createReadStream(filePath, { encoding: "utf-8" });
15551
+ const stream = fs15.createReadStream(filePath, { encoding: "utf-8" });
15029
15552
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
15030
15553
  try {
15031
15554
  for await (const line of rl) {
@@ -15047,12 +15570,12 @@ async function extractCwdFromJsonl(filePath) {
15047
15570
  async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
15048
15571
  let entries;
15049
15572
  try {
15050
- entries = await fs14.promises.readdir(dir, { withFileTypes: true });
15573
+ entries = await fs15.promises.readdir(dir, { withFileTypes: true });
15051
15574
  } catch {
15052
15575
  return;
15053
15576
  }
15054
15577
  for (const entry of entries) {
15055
- const fullPath = path18.join(dir, entry.name);
15578
+ const fullPath = path19.join(dir, entry.name);
15056
15579
  if (entry.isDirectory()) {
15057
15580
  if (SKIP_DIRS.has(entry.name)) continue;
15058
15581
  await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
@@ -15074,19 +15597,19 @@ function fallbackDecode(encodedName) {
15074
15597
  var ClaudeSource = class {
15075
15598
  name = "claude";
15076
15599
  async scan() {
15077
- const baseDir = path18.join(os9.homedir(), ".claude", "projects");
15600
+ const baseDir = path19.join(os9.homedir(), ".claude", "projects");
15078
15601
  try {
15079
- await fs14.promises.access(baseDir);
15602
+ await fs15.promises.access(baseDir);
15080
15603
  } catch {
15081
15604
  return [];
15082
15605
  }
15083
- const projectDirs = await fs14.promises.readdir(baseDir, {
15606
+ const projectDirs = await fs15.promises.readdir(baseDir, {
15084
15607
  withFileTypes: true
15085
15608
  });
15086
15609
  const dirEntries = projectDirs.filter((d) => d.isDirectory());
15087
15610
  const resultArrays = await Promise.all(
15088
15611
  dirEntries.map(async (dir) => {
15089
- const projectPath = path18.join(baseDir, dir.name);
15612
+ const projectPath = path19.join(baseDir, dir.name);
15090
15613
  const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
15091
15614
  const files = [];
15092
15615
  await collectFiles(
@@ -15104,12 +15627,12 @@ var ClaudeSource = class {
15104
15627
  };
15105
15628
 
15106
15629
  // src/sources/codex.ts
15107
- import fs15 from "fs";
15630
+ import fs16 from "fs";
15108
15631
  import os10 from "os";
15109
- import path19 from "path";
15632
+ import path20 from "path";
15110
15633
  import readline2 from "readline";
15111
15634
  async function parseSessionMeta(filePath) {
15112
- const stream = fs15.createReadStream(filePath, { encoding: "utf-8" });
15635
+ const stream = fs16.createReadStream(filePath, { encoding: "utf-8" });
15113
15636
  const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
15114
15637
  try {
15115
15638
  for await (const line of rl) {
@@ -15134,12 +15657,12 @@ async function findJsonlFiles(dir) {
15134
15657
  async function walk(d) {
15135
15658
  let entries;
15136
15659
  try {
15137
- entries = await fs15.promises.readdir(d, { withFileTypes: true });
15660
+ entries = await fs16.promises.readdir(d, { withFileTypes: true });
15138
15661
  } catch {
15139
15662
  return;
15140
15663
  }
15141
15664
  for (const entry of entries) {
15142
- const full = path19.join(d, entry.name);
15665
+ const full = path20.join(d, entry.name);
15143
15666
  if (entry.isDirectory()) {
15144
15667
  await walk(full);
15145
15668
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -15153,11 +15676,11 @@ async function findJsonlFiles(dir) {
15153
15676
  async function loadHistory(historyPath) {
15154
15677
  const map = /* @__PURE__ */ new Map();
15155
15678
  try {
15156
- await fs15.promises.access(historyPath);
15679
+ await fs16.promises.access(historyPath);
15157
15680
  } catch {
15158
15681
  return map;
15159
15682
  }
15160
- const stream = fs15.createReadStream(historyPath, { encoding: "utf-8" });
15683
+ const stream = fs16.createReadStream(historyPath, { encoding: "utf-8" });
15161
15684
  const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
15162
15685
  try {
15163
15686
  for await (const line of rl) {
@@ -15184,14 +15707,14 @@ async function loadHistory(historyPath) {
15184
15707
  var CodexSource = class {
15185
15708
  name = "codex";
15186
15709
  async scan() {
15187
- const codexDir = path19.join(os10.homedir(), ".codex");
15188
- const sessionsDir = path19.join(codexDir, "sessions");
15710
+ const codexDir = path20.join(os10.homedir(), ".codex");
15711
+ const sessionsDir = path20.join(codexDir, "sessions");
15189
15712
  try {
15190
- await fs15.promises.access(sessionsDir);
15713
+ await fs16.promises.access(sessionsDir);
15191
15714
  } catch {
15192
15715
  return [];
15193
15716
  }
15194
- const historyPath = path19.join(codexDir, "history.jsonl");
15717
+ const historyPath = path20.join(codexDir, "history.jsonl");
15195
15718
  const [jsonlFiles, historyMap] = await Promise.all([
15196
15719
  findJsonlFiles(sessionsDir),
15197
15720
  loadHistory(historyPath)
@@ -15214,8 +15737,8 @@ var CodexSource = class {
15214
15737
  });
15215
15738
  const historyLines = historyMap.get(meta.sessionId);
15216
15739
  if (historyLines) {
15217
- const sessionDir = path19.relative(sessionsDir, path19.dirname(filePath));
15218
- const historyAbsPath = path19.join(
15740
+ const sessionDir = path20.relative(sessionsDir, path20.dirname(filePath));
15741
+ const historyAbsPath = path20.join(
15219
15742
  sessionsDir,
15220
15743
  sessionDir,
15221
15744
  `history-${meta.sessionId}.jsonl`
@@ -15235,18 +15758,18 @@ var CodexSource = class {
15235
15758
  };
15236
15759
 
15237
15760
  // src/sources/copilotChat.ts
15238
- import fs16 from "fs";
15761
+ import fs17 from "fs";
15239
15762
  import os11 from "os";
15240
- import path20 from "path";
15763
+ import path21 from "path";
15241
15764
  import { fileURLToPath } from "url";
15242
15765
  function vsCodeUserDirs() {
15243
15766
  const home = os11.homedir();
15244
15767
  const dirs = [
15245
- path20.join(home, "Library", "Application Support", "Code", "User"),
15246
- path20.join(home, ".config", "Code", "User")
15768
+ path21.join(home, "Library", "Application Support", "Code", "User"),
15769
+ path21.join(home, ".config", "Code", "User")
15247
15770
  ];
15248
15771
  if (process.env.APPDATA) {
15249
- dirs.push(path20.join(process.env.APPDATA, "Code", "User"));
15772
+ dirs.push(path21.join(process.env.APPDATA, "Code", "User"));
15250
15773
  }
15251
15774
  return dirs;
15252
15775
  }
@@ -15261,7 +15784,7 @@ function uriToFsPath(uri) {
15261
15784
  async function readWorkspaceFolder(workspaceJsonPath) {
15262
15785
  let raw;
15263
15786
  try {
15264
- raw = await fs16.promises.readFile(workspaceJsonPath, "utf-8");
15787
+ raw = await fs17.promises.readFile(workspaceJsonPath, "utf-8");
15265
15788
  } catch {
15266
15789
  return null;
15267
15790
  }
@@ -15283,10 +15806,10 @@ var CopilotChatSource = class {
15283
15806
  async scan() {
15284
15807
  const results = [];
15285
15808
  for (const userDir of vsCodeUserDirs()) {
15286
- const workspaceStorage = path20.join(userDir, "workspaceStorage");
15809
+ const workspaceStorage = path21.join(userDir, "workspaceStorage");
15287
15810
  let hashDirs;
15288
15811
  try {
15289
- hashDirs = await fs16.promises.readdir(workspaceStorage, {
15812
+ hashDirs = await fs17.promises.readdir(workspaceStorage, {
15290
15813
  withFileTypes: true
15291
15814
  });
15292
15815
  } catch {
@@ -15294,22 +15817,22 @@ var CopilotChatSource = class {
15294
15817
  }
15295
15818
  for (const hash of hashDirs) {
15296
15819
  if (!hash.isDirectory()) continue;
15297
- const wsRoot = path20.join(workspaceStorage, hash.name);
15298
- const transcriptsDir = path20.join(
15820
+ const wsRoot = path21.join(workspaceStorage, hash.name);
15821
+ const transcriptsDir = path21.join(
15299
15822
  wsRoot,
15300
15823
  "GitHub.copilot-chat",
15301
15824
  "transcripts"
15302
15825
  );
15303
15826
  let transcriptEntries;
15304
15827
  try {
15305
- transcriptEntries = await fs16.promises.readdir(transcriptsDir, {
15828
+ transcriptEntries = await fs17.promises.readdir(transcriptsDir, {
15306
15829
  withFileTypes: true
15307
15830
  });
15308
15831
  } catch {
15309
15832
  continue;
15310
15833
  }
15311
15834
  const repoPath = await readWorkspaceFolder(
15312
- path20.join(wsRoot, "workspace.json")
15835
+ path21.join(wsRoot, "workspace.json")
15313
15836
  );
15314
15837
  if (!repoPath) continue;
15315
15838
  for (const entry of transcriptEntries) {
@@ -15317,7 +15840,7 @@ var CopilotChatSource = class {
15317
15840
  const sessionId = entry.name.slice(0, -".jsonl".length);
15318
15841
  results.push({
15319
15842
  sourceName: this.name,
15320
- absolutePath: path20.join(transcriptsDir, entry.name),
15843
+ absolutePath: path21.join(transcriptsDir, entry.name),
15321
15844
  repoPath,
15322
15845
  metadata: { sessionId }
15323
15846
  });
@@ -15357,7 +15880,7 @@ function reportRedactionStats(noun, stats) {
15357
15880
  async function filterByTimeRange(group, range) {
15358
15881
  const results = await Promise.all(
15359
15882
  group.files.map(
15360
- (f) => fs17.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
15883
+ (f) => fs18.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
15361
15884
  )
15362
15885
  );
15363
15886
  const filtered = [];
@@ -15384,10 +15907,10 @@ async function runInteractive() {
15384
15907
  s.start(`Scanning ${source.name} logs...`);
15385
15908
  const allFiles = await source.scan();
15386
15909
  const allGroups = await mergeByRepo(allFiles);
15387
- const repoRoot = path21.resolve(repo.root);
15910
+ const repoRoot = path22.resolve(repo.root);
15388
15911
  const matching = allGroups.filter((g) => {
15389
- const resolved = path21.resolve(g.repoPath);
15390
- return resolved === repoRoot || resolved.startsWith(repoRoot + path21.sep);
15912
+ const resolved = path22.resolve(g.repoPath);
15913
+ return resolved === repoRoot || resolved.startsWith(repoRoot + path22.sep);
15391
15914
  });
15392
15915
  if (matching.length === 0) {
15393
15916
  s.stop(`No ${source.name} logs found for ${repo.name}.`);
@@ -15418,7 +15941,7 @@ async function runInteractive() {
15418
15941
  }
15419
15942
  }
15420
15943
  const envFileNames = await discoverEnvFiles(repoRoot);
15421
- const envFilePaths = envFileNames.map((n) => path21.join(repoRoot, n));
15944
+ const envFilePaths = envFileNames.map((n) => path22.join(repoRoot, n));
15422
15945
  const additionalFiles = await promptSecretFiles(envFileNames);
15423
15946
  const secretResult = await collectSecrets(
15424
15947
  repoRoot,