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