hillclimb 0.2.0 → 0.3.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 +985 -305
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import fs17 from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path21 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) {
|
|
1083
1098
|
switch (format) {
|
|
1084
1099
|
case "cursor":
|
|
1085
|
-
return
|
|
1100
|
+
return (settings.hooks?.[eventName] ?? []).map((e) => e.command).filter((command) => typeof command === "string");
|
|
1086
1101
|
case "copilot":
|
|
1087
|
-
return
|
|
1102
|
+
return (settings.hooks?.[eventName] ?? []).filter((e) => e.type === "command").map((e) => e.command).filter((command) => typeof command === "string");
|
|
1088
1103
|
default:
|
|
1089
|
-
return
|
|
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) {
|
|
1108
|
+
switch (format) {
|
|
1109
|
+
case "cursor":
|
|
1110
|
+
return cursorUninstallMatching(settings, eventName, predicate);
|
|
1111
|
+
case "copilot":
|
|
1112
|
+
return copilotUninstallMatching(settings, eventName, predicate);
|
|
1113
|
+
default:
|
|
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
|
}
|
|
@@ -11304,6 +11367,59 @@ function parseOutputBlob(raw) {
|
|
|
11304
11367
|
}
|
|
11305
11368
|
return [String(parsed), void 0];
|
|
11306
11369
|
}
|
|
11370
|
+
function asObject(value) {
|
|
11371
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
11372
|
+
}
|
|
11373
|
+
function compactExtra(extra) {
|
|
11374
|
+
const compacted = {};
|
|
11375
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
11376
|
+
if (value !== void 0 && value !== null) compacted[key] = value;
|
|
11377
|
+
}
|
|
11378
|
+
return Object.keys(compacted).length > 0 ? compacted : void 0;
|
|
11379
|
+
}
|
|
11380
|
+
function parseJsonObject(raw) {
|
|
11381
|
+
const obj = asObject(raw);
|
|
11382
|
+
if (obj) return obj;
|
|
11383
|
+
if (typeof raw !== "string") return void 0;
|
|
11384
|
+
try {
|
|
11385
|
+
return asObject(JSON.parse(raw));
|
|
11386
|
+
} catch {
|
|
11387
|
+
return void 0;
|
|
11388
|
+
}
|
|
11389
|
+
}
|
|
11390
|
+
function subagentRefFromSpawnOutput(args, rawOutput) {
|
|
11391
|
+
const output = parseJsonObject(rawOutput);
|
|
11392
|
+
const sessionId = typeof output?.agent_id === "string" && output.agent_id || typeof output?.agent_path === "string" && output.agent_path || void 0;
|
|
11393
|
+
if (!sessionId) return void 0;
|
|
11394
|
+
const extra = compactExtra({
|
|
11395
|
+
agent_role: typeof args?.agent_type === "string" && args.agent_type || typeof output?.agent_role === "string" && output.agent_role || void 0,
|
|
11396
|
+
nickname: typeof output?.nickname === "string" && output.nickname || typeof output?.agent_nickname === "string" && output.agent_nickname || void 0
|
|
11397
|
+
});
|
|
11398
|
+
return {
|
|
11399
|
+
session_id: sessionId,
|
|
11400
|
+
extra
|
|
11401
|
+
};
|
|
11402
|
+
}
|
|
11403
|
+
function codexTrajectoryExtra(metaPayload) {
|
|
11404
|
+
const source = asObject(metaPayload.source);
|
|
11405
|
+
const subagent = asObject(source?.subagent);
|
|
11406
|
+
const threadSpawn = asObject(subagent?.thread_spawn);
|
|
11407
|
+
const parentThreadId = typeof threadSpawn?.parent_thread_id === "string" ? threadSpawn.parent_thread_id : void 0;
|
|
11408
|
+
const subagentExtra = threadSpawn ? compactExtra({
|
|
11409
|
+
depth: threadSpawn.depth,
|
|
11410
|
+
agent_path: threadSpawn.agent_path,
|
|
11411
|
+
agent_nickname: threadSpawn.agent_nickname,
|
|
11412
|
+
agent_role: threadSpawn.agent_role
|
|
11413
|
+
}) : void 0;
|
|
11414
|
+
return compactExtra({
|
|
11415
|
+
thread_source: metaPayload.thread_source,
|
|
11416
|
+
agent_nickname: metaPayload.agent_nickname,
|
|
11417
|
+
agent_role: metaPayload.agent_role,
|
|
11418
|
+
parent_session_id: parentThreadId,
|
|
11419
|
+
parent_thread_id: parentThreadId,
|
|
11420
|
+
subagent: subagentExtra
|
|
11421
|
+
});
|
|
11422
|
+
}
|
|
11307
11423
|
function convertCodexToTrajectory(jsonlContent, sessionId) {
|
|
11308
11424
|
const rawEvents = [];
|
|
11309
11425
|
for (const line of jsonlContent.split("\n")) {
|
|
@@ -11317,7 +11433,7 @@ function convertCodexToTrajectory(jsonlContent, sessionId) {
|
|
|
11317
11433
|
if (rawEvents.length === 0) return null;
|
|
11318
11434
|
const sessionMeta = rawEvents.find((e) => e.type === "session_meta");
|
|
11319
11435
|
const metaPayload = sessionMeta?.payload ?? {};
|
|
11320
|
-
const sid = sessionId ?? metaPayload.id
|
|
11436
|
+
const sid = sessionId ?? (typeof metaPayload.id === "string" ? metaPayload.id : "");
|
|
11321
11437
|
const agentVersion = metaPayload.cli_version ?? "unknown";
|
|
11322
11438
|
const agentExtra = {};
|
|
11323
11439
|
for (const key of ["originator", "cwd", "git", "instructions"]) {
|
|
@@ -11434,6 +11550,13 @@ function convertCodexToTrajectory(jsonlContent, sessionId) {
|
|
|
11434
11550
|
callInfo.output = outputText;
|
|
11435
11551
|
callInfo.metadata = metadata;
|
|
11436
11552
|
callInfo.timestamp = callInfo.timestamp ?? timestamp;
|
|
11553
|
+
if (callInfo.tool_name === "spawn_agent") {
|
|
11554
|
+
const subagentRef = subagentRefFromSpawnOutput(
|
|
11555
|
+
callInfo.arguments,
|
|
11556
|
+
payload.output
|
|
11557
|
+
);
|
|
11558
|
+
if (subagentRef) callInfo.subagentRefs = [subagentRef];
|
|
11559
|
+
}
|
|
11437
11560
|
normalizedEvents.push(callInfo);
|
|
11438
11561
|
pendingReasoning = void 0;
|
|
11439
11562
|
}
|
|
@@ -11489,7 +11612,8 @@ function convertCodexToTrajectory(jsonlContent, sessionId) {
|
|
|
11489
11612
|
extra: Object.keys(agentExtra).length > 0 ? agentExtra : void 0
|
|
11490
11613
|
},
|
|
11491
11614
|
steps,
|
|
11492
|
-
final_metrics: finalMetrics
|
|
11615
|
+
final_metrics: finalMetrics,
|
|
11616
|
+
extra: codexTrajectoryExtra(metaPayload)
|
|
11493
11617
|
};
|
|
11494
11618
|
}
|
|
11495
11619
|
function convertEventToStep2(event, stepId, defaultModelName) {
|
|
@@ -11524,7 +11648,8 @@ function convertEventToStep2(event, stepId, defaultModelName) {
|
|
|
11524
11648
|
if (event.output !== void 0) {
|
|
11525
11649
|
const result = {
|
|
11526
11650
|
source_call_id: callId || void 0,
|
|
11527
|
-
content: event.output
|
|
11651
|
+
content: event.output,
|
|
11652
|
+
subagent_trajectory_ref: event.subagentRefs
|
|
11528
11653
|
};
|
|
11529
11654
|
observation = { results: [result] };
|
|
11530
11655
|
}
|
|
@@ -11572,13 +11697,13 @@ function excludeNone(obj) {
|
|
|
11572
11697
|
}
|
|
11573
11698
|
|
|
11574
11699
|
// src/normalizer/copilotChat.ts
|
|
11575
|
-
function
|
|
11700
|
+
function asObject2(value) {
|
|
11576
11701
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
11577
11702
|
}
|
|
11578
11703
|
function asNumber(value) {
|
|
11579
11704
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
11580
11705
|
}
|
|
11581
|
-
function
|
|
11706
|
+
function compactExtra2(extra) {
|
|
11582
11707
|
const result = {};
|
|
11583
11708
|
for (const [key, value] of Object.entries(extra)) {
|
|
11584
11709
|
if (value !== void 0 && value !== null) result[key] = value;
|
|
@@ -11591,11 +11716,11 @@ function parseJsonLines(content) {
|
|
|
11591
11716
|
const trimmed = line.trim();
|
|
11592
11717
|
if (!trimmed) continue;
|
|
11593
11718
|
try {
|
|
11594
|
-
const parsed =
|
|
11719
|
+
const parsed = asObject2(JSON.parse(trimmed));
|
|
11595
11720
|
if (!parsed) continue;
|
|
11596
11721
|
entries.push({
|
|
11597
11722
|
type: typeof parsed.type === "string" ? parsed.type : void 0,
|
|
11598
|
-
data:
|
|
11723
|
+
data: asObject2(parsed.data),
|
|
11599
11724
|
id: typeof parsed.id === "string" ? parsed.id : void 0,
|
|
11600
11725
|
timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : void 0
|
|
11601
11726
|
});
|
|
@@ -11605,11 +11730,11 @@ function parseJsonLines(content) {
|
|
|
11605
11730
|
return entries;
|
|
11606
11731
|
}
|
|
11607
11732
|
function parseArguments(value) {
|
|
11608
|
-
const obj =
|
|
11733
|
+
const obj = asObject2(value);
|
|
11609
11734
|
if (obj) return obj;
|
|
11610
11735
|
if (typeof value === "string") {
|
|
11611
11736
|
try {
|
|
11612
|
-
const parsed =
|
|
11737
|
+
const parsed = asObject2(JSON.parse(value));
|
|
11613
11738
|
if (parsed) return parsed;
|
|
11614
11739
|
} catch {
|
|
11615
11740
|
return value ? { input: value } : {};
|
|
@@ -11631,7 +11756,7 @@ function buildMetrics2(data) {
|
|
|
11631
11756
|
completion_tokens: outputTokens || void 0,
|
|
11632
11757
|
cached_tokens: cacheReadTokens || void 0,
|
|
11633
11758
|
cost_usd: cost || void 0,
|
|
11634
|
-
extra:
|
|
11759
|
+
extra: compactExtra2({
|
|
11635
11760
|
cache_write_tokens: cacheWriteTokens || void 0,
|
|
11636
11761
|
duration_ms: data.duration,
|
|
11637
11762
|
initiator: data.initiator,
|
|
@@ -11663,7 +11788,7 @@ function finalMetricsFromSteps(steps) {
|
|
|
11663
11788
|
};
|
|
11664
11789
|
}
|
|
11665
11790
|
function makeToolCall(request) {
|
|
11666
|
-
const req =
|
|
11791
|
+
const req = asObject2(request);
|
|
11667
11792
|
if (!req) return null;
|
|
11668
11793
|
const callId = typeof req.toolCallId === "string" && req.toolCallId || typeof req.id === "string" && req.id || "";
|
|
11669
11794
|
const name = typeof req.name === "string" && req.name || typeof req.toolName === "string" && req.toolName || "tool";
|
|
@@ -11683,7 +11808,7 @@ function toolCallFromExecutionStart(data) {
|
|
|
11683
11808
|
};
|
|
11684
11809
|
}
|
|
11685
11810
|
function contentFromToolResult(data) {
|
|
11686
|
-
const result =
|
|
11811
|
+
const result = asObject2(data.result);
|
|
11687
11812
|
if (typeof result?.content === "string") return result.content;
|
|
11688
11813
|
if (typeof data.content === "string") return data.content;
|
|
11689
11814
|
return void 0;
|
|
@@ -11709,7 +11834,7 @@ function convertCopilotChatToTrajectory(jsonlContent, sessionId) {
|
|
|
11709
11834
|
copilotVersion = data.copilotVersion;
|
|
11710
11835
|
if (typeof data.vscodeVersion === "string")
|
|
11711
11836
|
vscodeVersion = data.vscodeVersion;
|
|
11712
|
-
const context =
|
|
11837
|
+
const context = asObject2(data.context);
|
|
11713
11838
|
if (typeof context?.cwd === "string") cwd = context.cwd;
|
|
11714
11839
|
continue;
|
|
11715
11840
|
}
|
|
@@ -11722,7 +11847,7 @@ function convertCopilotChatToTrajectory(jsonlContent, sessionId) {
|
|
|
11722
11847
|
timestamp: entry.timestamp,
|
|
11723
11848
|
source,
|
|
11724
11849
|
message: content,
|
|
11725
|
-
extra:
|
|
11850
|
+
extra: compactExtra2({
|
|
11726
11851
|
attachments: data.attachments,
|
|
11727
11852
|
source: data.source,
|
|
11728
11853
|
agent_mode: data.agentMode,
|
|
@@ -11752,7 +11877,7 @@ function convertCopilotChatToTrajectory(jsonlContent, sessionId) {
|
|
|
11752
11877
|
source: "agent",
|
|
11753
11878
|
message: content || (toolCalls.length > 0 ? "(tool use)" : ""),
|
|
11754
11879
|
model_name: defaultModelName,
|
|
11755
|
-
extra:
|
|
11880
|
+
extra: compactExtra2({
|
|
11756
11881
|
message_id: data.messageId,
|
|
11757
11882
|
phase: data.phase,
|
|
11758
11883
|
output_tokens: data.outputTokens
|
|
@@ -11798,7 +11923,7 @@ function convertCopilotChatToTrajectory(jsonlContent, sessionId) {
|
|
|
11798
11923
|
target.observation = observation;
|
|
11799
11924
|
const extra = { ...target.extra ?? {} };
|
|
11800
11925
|
extra.tool_success = data.success;
|
|
11801
|
-
target.extra =
|
|
11926
|
+
target.extra = compactExtra2(extra);
|
|
11802
11927
|
}
|
|
11803
11928
|
if (callId) pendingToolSteps.delete(callId);
|
|
11804
11929
|
continue;
|
|
@@ -11824,7 +11949,7 @@ function convertCopilotChatToTrajectory(jsonlContent, sessionId) {
|
|
|
11824
11949
|
name: "github-copilot-chat",
|
|
11825
11950
|
version: copilotVersion,
|
|
11826
11951
|
model_name: defaultModelName,
|
|
11827
|
-
extra:
|
|
11952
|
+
extra: compactExtra2({
|
|
11828
11953
|
vscode_version: vscodeVersion,
|
|
11829
11954
|
cwd
|
|
11830
11955
|
})
|
|
@@ -11892,7 +12017,7 @@ function convertCursorToTrajectory(jsonlContent, sessionId) {
|
|
|
11892
12017
|
}
|
|
11893
12018
|
|
|
11894
12019
|
// src/normalizer/opencode.ts
|
|
11895
|
-
function
|
|
12020
|
+
function asObject3(value) {
|
|
11896
12021
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
11897
12022
|
}
|
|
11898
12023
|
function asArray(value) {
|
|
@@ -11901,7 +12026,7 @@ function asArray(value) {
|
|
|
11901
12026
|
function asNumber2(value) {
|
|
11902
12027
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
11903
12028
|
}
|
|
11904
|
-
function
|
|
12029
|
+
function compactExtra3(extra) {
|
|
11905
12030
|
const result = {};
|
|
11906
12031
|
for (const [key, value] of Object.entries(extra)) {
|
|
11907
12032
|
if (value !== void 0 && value !== null) result[key] = value;
|
|
@@ -11915,7 +12040,7 @@ function parseJsonLines2(content) {
|
|
|
11915
12040
|
if (!trimmed) continue;
|
|
11916
12041
|
try {
|
|
11917
12042
|
const parsed = JSON.parse(trimmed);
|
|
11918
|
-
const obj =
|
|
12043
|
+
const obj = asObject3(parsed);
|
|
11919
12044
|
if (obj) events.push(obj);
|
|
11920
12045
|
} catch {
|
|
11921
12046
|
}
|
|
@@ -11930,7 +12055,7 @@ function timestampToIso(value) {
|
|
|
11930
12055
|
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
11931
12056
|
}
|
|
11932
12057
|
function timeFromObject(value) {
|
|
11933
|
-
const obj =
|
|
12058
|
+
const obj = asObject3(value);
|
|
11934
12059
|
if (!obj) return void 0;
|
|
11935
12060
|
return timestampToIso(obj.start ?? obj.created ?? obj.completed ?? obj.end);
|
|
11936
12061
|
}
|
|
@@ -11943,12 +12068,12 @@ function stringify2(value) {
|
|
|
11943
12068
|
}
|
|
11944
12069
|
}
|
|
11945
12070
|
function argsFromUnknown(value) {
|
|
11946
|
-
const obj =
|
|
12071
|
+
const obj = asObject3(value);
|
|
11947
12072
|
if (obj) return obj;
|
|
11948
12073
|
if (typeof value === "string") {
|
|
11949
12074
|
try {
|
|
11950
12075
|
const parsed = JSON.parse(value);
|
|
11951
|
-
const parsedObj =
|
|
12076
|
+
const parsedObj = asObject3(parsed);
|
|
11952
12077
|
if (parsedObj) return parsedObj;
|
|
11953
12078
|
} catch {
|
|
11954
12079
|
return value ? { input: value } : {};
|
|
@@ -11957,16 +12082,16 @@ function argsFromUnknown(value) {
|
|
|
11957
12082
|
return value === void 0 || value === null ? {} : { value };
|
|
11958
12083
|
}
|
|
11959
12084
|
function modelNameFromInfo(info) {
|
|
11960
|
-
const model =
|
|
12085
|
+
const model = asObject3(info.model);
|
|
11961
12086
|
const modelID = typeof info.modelID === "string" && info.modelID || typeof model?.modelID === "string" && model.modelID || void 0;
|
|
11962
12087
|
const providerID = typeof info.providerID === "string" && info.providerID || typeof model?.providerID === "string" && model.providerID || void 0;
|
|
11963
12088
|
if (providerID && modelID) return `${providerID}/${modelID}`;
|
|
11964
12089
|
return modelID;
|
|
11965
12090
|
}
|
|
11966
12091
|
function metricsFromTokens(tokens, cost) {
|
|
11967
|
-
const t =
|
|
12092
|
+
const t = asObject3(tokens);
|
|
11968
12093
|
if (!t) return void 0;
|
|
11969
|
-
const cache =
|
|
12094
|
+
const cache = asObject3(t.cache);
|
|
11970
12095
|
const input = asNumber2(t.input) ?? 0;
|
|
11971
12096
|
const output = asNumber2(t.output) ?? 0;
|
|
11972
12097
|
const reasoning = asNumber2(t.reasoning) ?? 0;
|
|
@@ -11976,7 +12101,7 @@ function metricsFromTokens(tokens, cost) {
|
|
|
11976
12101
|
if (!input && !output && !cacheRead && !cacheWrite && !costUsd) {
|
|
11977
12102
|
return void 0;
|
|
11978
12103
|
}
|
|
11979
|
-
const extra =
|
|
12104
|
+
const extra = compactExtra3({
|
|
11980
12105
|
reasoning_tokens: reasoning || void 0,
|
|
11981
12106
|
cache_write_tokens: cacheWrite || void 0
|
|
11982
12107
|
});
|
|
@@ -12017,12 +12142,12 @@ function finalMetricsFromSteps2(steps) {
|
|
|
12017
12142
|
};
|
|
12018
12143
|
}
|
|
12019
12144
|
function entryFromLine(line) {
|
|
12020
|
-
const info =
|
|
12145
|
+
const info = asObject3(line.info);
|
|
12021
12146
|
if (info) {
|
|
12022
12147
|
return {
|
|
12023
12148
|
info,
|
|
12024
12149
|
parts: asArray(line.parts).flatMap((part) => {
|
|
12025
|
-
const obj =
|
|
12150
|
+
const obj = asObject3(part);
|
|
12026
12151
|
return obj ? [obj] : [];
|
|
12027
12152
|
})
|
|
12028
12153
|
};
|
|
@@ -12031,7 +12156,7 @@ function entryFromLine(line) {
|
|
|
12031
12156
|
return {
|
|
12032
12157
|
info: line,
|
|
12033
12158
|
parts: asArray(line.parts).flatMap((part) => {
|
|
12034
|
-
const obj =
|
|
12159
|
+
const obj = asObject3(part);
|
|
12035
12160
|
return obj ? [obj] : [];
|
|
12036
12161
|
})
|
|
12037
12162
|
};
|
|
@@ -12050,9 +12175,9 @@ function entriesFromEventWrappers(lines) {
|
|
|
12050
12175
|
}
|
|
12051
12176
|
for (const line of lines) {
|
|
12052
12177
|
const type = line.type;
|
|
12053
|
-
const props =
|
|
12178
|
+
const props = asObject3(line.properties) ?? line;
|
|
12054
12179
|
if (type === "message.updated") {
|
|
12055
|
-
const info =
|
|
12180
|
+
const info = asObject3(props.info);
|
|
12056
12181
|
const id = typeof info?.id === "string" ? info.id : void 0;
|
|
12057
12182
|
if (!info || !id) continue;
|
|
12058
12183
|
const entry = getEntry(id, info.sessionID);
|
|
@@ -12060,7 +12185,7 @@ function entriesFromEventWrappers(lines) {
|
|
|
12060
12185
|
continue;
|
|
12061
12186
|
}
|
|
12062
12187
|
if (type === "message.part.updated") {
|
|
12063
|
-
const part =
|
|
12188
|
+
const part = asObject3(props.part);
|
|
12064
12189
|
const messageID = typeof part?.messageID === "string" && part.messageID || void 0;
|
|
12065
12190
|
if (!part || !messageID) continue;
|
|
12066
12191
|
const entry = getEntry(messageID, part.sessionID);
|
|
@@ -12085,8 +12210,8 @@ function getAgentVersion(exportInfo) {
|
|
|
12085
12210
|
}
|
|
12086
12211
|
function sortEntries(entries) {
|
|
12087
12212
|
return [...entries].sort((a, b) => {
|
|
12088
|
-
const at = asNumber2(
|
|
12089
|
-
const bt = asNumber2(
|
|
12213
|
+
const at = asNumber2(asObject3(a.info.time)?.created) ?? 0;
|
|
12214
|
+
const bt = asNumber2(asObject3(b.info.time)?.created) ?? 0;
|
|
12090
12215
|
return at - bt;
|
|
12091
12216
|
});
|
|
12092
12217
|
}
|
|
@@ -12143,7 +12268,7 @@ function buildUserStep(entry, stepId, defaultModelName) {
|
|
|
12143
12268
|
source: "user",
|
|
12144
12269
|
message,
|
|
12145
12270
|
model_name: defaultModelName,
|
|
12146
|
-
extra:
|
|
12271
|
+
extra: compactExtra3(extra)
|
|
12147
12272
|
};
|
|
12148
12273
|
}
|
|
12149
12274
|
function buildAgentStep(parts, info, stepId, defaultModelName, fallbackMetrics) {
|
|
@@ -12173,7 +12298,7 @@ function buildAgentStep(parts, info, stepId, defaultModelName, fallbackMetrics)
|
|
|
12173
12298
|
break;
|
|
12174
12299
|
}
|
|
12175
12300
|
case "tool": {
|
|
12176
|
-
const state =
|
|
12301
|
+
const state = asObject3(part.state) ?? {};
|
|
12177
12302
|
const callID = typeof part.callID === "string" && part.callID || typeof part.id === "string" && part.id || "";
|
|
12178
12303
|
const toolName = typeof part.tool === "string" && part.tool || "tool";
|
|
12179
12304
|
const input = argsFromUnknown(state.input);
|
|
@@ -12227,7 +12352,7 @@ function buildAgentStep(parts, info, stepId, defaultModelName, fallbackMetrics)
|
|
|
12227
12352
|
if (toolCalls.length > 0) step.tool_calls = toolCalls;
|
|
12228
12353
|
if (observation) step.observation = observation;
|
|
12229
12354
|
if (metrics) step.metrics = metrics;
|
|
12230
|
-
const compactedExtra =
|
|
12355
|
+
const compactedExtra = compactExtra3(extra);
|
|
12231
12356
|
if (compactedExtra) step.extra = compactedExtra;
|
|
12232
12357
|
return step;
|
|
12233
12358
|
}
|
|
@@ -12243,7 +12368,7 @@ function buildUnavailableAgentStep(info, stepId, defaultModelName) {
|
|
|
12243
12368
|
message: "(message unavailable)",
|
|
12244
12369
|
model_name: modelNameFromInfo(info) ?? defaultModelName,
|
|
12245
12370
|
metrics,
|
|
12246
|
-
extra:
|
|
12371
|
+
extra: compactExtra3({
|
|
12247
12372
|
content_unavailable: true,
|
|
12248
12373
|
finish_reason: info.finish,
|
|
12249
12374
|
error: info.error
|
|
@@ -12313,14 +12438,14 @@ function convertRunEventsToTrajectory(events, sessionId) {
|
|
|
12313
12438
|
}
|
|
12314
12439
|
if (type === "step_finish") {
|
|
12315
12440
|
if (current) {
|
|
12316
|
-
current.finish =
|
|
12441
|
+
current.finish = asObject3(event.part) ?? {};
|
|
12317
12442
|
turns.push(current);
|
|
12318
12443
|
current = null;
|
|
12319
12444
|
}
|
|
12320
12445
|
continue;
|
|
12321
12446
|
}
|
|
12322
12447
|
if (current && (type === "text" || type === "reasoning" || type === "tool_use")) {
|
|
12323
|
-
const part =
|
|
12448
|
+
const part = asObject3(event.part);
|
|
12324
12449
|
if (part) current.parts.push(part);
|
|
12325
12450
|
}
|
|
12326
12451
|
}
|
|
@@ -12362,17 +12487,17 @@ function convertOpenCodeToTrajectory(content, sessionId) {
|
|
|
12362
12487
|
if (!trimmed) return null;
|
|
12363
12488
|
try {
|
|
12364
12489
|
const parsed = JSON.parse(trimmed);
|
|
12365
|
-
const parsedObj =
|
|
12490
|
+
const parsedObj = asObject3(parsed);
|
|
12366
12491
|
const messages = asArray(parsedObj?.messages);
|
|
12367
12492
|
if (parsedObj && messages.length > 0) {
|
|
12368
12493
|
const entries2 = messages.flatMap((message) => {
|
|
12369
|
-
const entry = entryFromLine(
|
|
12494
|
+
const entry = entryFromLine(asObject3(message) ?? {});
|
|
12370
12495
|
return entry ? [entry] : [];
|
|
12371
12496
|
});
|
|
12372
12497
|
return convertMessageEntriesToTrajectory(
|
|
12373
12498
|
entries2,
|
|
12374
12499
|
sessionId,
|
|
12375
|
-
|
|
12500
|
+
asObject3(parsedObj.info)
|
|
12376
12501
|
);
|
|
12377
12502
|
}
|
|
12378
12503
|
} catch {
|
|
@@ -12418,7 +12543,7 @@ var NormalizeMiddleware = class {
|
|
|
12418
12543
|
continue;
|
|
12419
12544
|
const content = file.content ? file.content.toString("utf-8") : null;
|
|
12420
12545
|
if (!content) continue;
|
|
12421
|
-
const sessionId = file.metadata?.sessionId ?? path9.basename(file.absolutePath, ".jsonl");
|
|
12546
|
+
const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 : path9.basename(file.absolutePath, ".jsonl"));
|
|
12422
12547
|
try {
|
|
12423
12548
|
const trajectory = normalizeContent(
|
|
12424
12549
|
file.sourceName,
|
|
@@ -12676,6 +12801,7 @@ function resolveSourceTool(payload) {
|
|
|
12676
12801
|
return "claude";
|
|
12677
12802
|
}
|
|
12678
12803
|
async function selfHealHook(repoRoot, tool) {
|
|
12804
|
+
if (process.env.HILLCLIMB_SKIP_HOOK_SELF_HEAL === "1") return;
|
|
12679
12805
|
try {
|
|
12680
12806
|
const result = await healHookForTool(repoRoot, tool);
|
|
12681
12807
|
if (result.skipped) {
|
|
@@ -12958,6 +13084,7 @@ import crypto2 from "crypto";
|
|
|
12958
13084
|
|
|
12959
13085
|
// src/git-traces/handlers.ts
|
|
12960
13086
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
13087
|
+
import path15 from "path";
|
|
12961
13088
|
|
|
12962
13089
|
// src/git-traces/git-ops.ts
|
|
12963
13090
|
import { execFileSync } from "child_process";
|
|
@@ -13212,12 +13339,34 @@ function detectTransitionKind(repoRoot, prevHeadSha, nextHeadSha) {
|
|
|
13212
13339
|
return "branch-switch";
|
|
13213
13340
|
}
|
|
13214
13341
|
}
|
|
13342
|
+
function parseDirtyFilesFromStatus(status) {
|
|
13343
|
+
const raw = Buffer.isBuffer(status) ? status.toString("utf-8") : status;
|
|
13344
|
+
if (!raw) return [];
|
|
13345
|
+
if (raw.includes("\0")) {
|
|
13346
|
+
const fields = raw.split("\0").filter(Boolean);
|
|
13347
|
+
const dirtyFiles = [];
|
|
13348
|
+
for (let i = 0; i < fields.length; i++) {
|
|
13349
|
+
const record = fields[i];
|
|
13350
|
+
if (record.length < 4) continue;
|
|
13351
|
+
const statusCode = record.slice(0, 2);
|
|
13352
|
+
dirtyFiles.push(record.slice(3));
|
|
13353
|
+
if (statusCode.includes("R") || statusCode.includes("C")) i++;
|
|
13354
|
+
}
|
|
13355
|
+
return dirtyFiles;
|
|
13356
|
+
}
|
|
13357
|
+
return raw.split("\n").filter(Boolean).map((line) => {
|
|
13358
|
+
const match = /^(..) (.*)$/.exec(line);
|
|
13359
|
+
const pathPart = match ? match[2] : line.trim();
|
|
13360
|
+
if (pathPart.includes(" -> ")) return pathPart.split(" -> ").at(-1) ?? "";
|
|
13361
|
+
return pathPart;
|
|
13362
|
+
}).filter(Boolean);
|
|
13363
|
+
}
|
|
13215
13364
|
function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind, startedAt) {
|
|
13216
13365
|
const headSha = safeGit(repoRoot, ["rev-parse", "HEAD"]) ?? "unknown";
|
|
13217
13366
|
const branch = safeGit(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? null;
|
|
13218
13367
|
const remoteUrl = safeGit(repoRoot, ["config", "--get", "remote.origin.url"]) ?? null;
|
|
13219
|
-
const statusRaw =
|
|
13220
|
-
const dirtyFiles = statusRaw
|
|
13368
|
+
const statusRaw = safeGitBuffer(repoRoot, ["status", "--porcelain=v1", "-z"]) ?? Buffer.alloc(0);
|
|
13369
|
+
const dirtyFiles = parseDirtyFilesFromStatus(statusRaw);
|
|
13221
13370
|
const authorName = safeGit(repoRoot, ["config", "user.name"]) ?? "";
|
|
13222
13371
|
const authorEmail = safeGit(repoRoot, ["config", "user.email"]) ?? "";
|
|
13223
13372
|
const commits = transitionKind === "commit" && prevHeadSha && headSha !== "unknown" ? enumerateCommits(repoRoot, prevHeadSha, headSha) : void 0;
|
|
@@ -13328,9 +13477,9 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
13328
13477
|
oldPath
|
|
13329
13478
|
});
|
|
13330
13479
|
} else {
|
|
13331
|
-
const
|
|
13332
|
-
indexByPath.set(
|
|
13333
|
-
files.push({ path:
|
|
13480
|
+
const path22 = parts[parts.length - 1];
|
|
13481
|
+
indexByPath.set(path22, files.length);
|
|
13482
|
+
files.push({ path: path22, status, additions: 0, deletions: 0 });
|
|
13334
13483
|
}
|
|
13335
13484
|
}
|
|
13336
13485
|
for (const line of numstat.split("\n")) {
|
|
@@ -13361,6 +13510,19 @@ function safeGit(repoRoot, args) {
|
|
|
13361
13510
|
return null;
|
|
13362
13511
|
}
|
|
13363
13512
|
}
|
|
13513
|
+
function safeGitBuffer(repoRoot, args) {
|
|
13514
|
+
try {
|
|
13515
|
+
return gitBuffer(repoRoot, args);
|
|
13516
|
+
} catch (err) {
|
|
13517
|
+
if (isGitTimeoutError(err)) {
|
|
13518
|
+
appendLog(
|
|
13519
|
+
"warn",
|
|
13520
|
+
`git-traces: optional metadata command skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
13521
|
+
);
|
|
13522
|
+
}
|
|
13523
|
+
return null;
|
|
13524
|
+
}
|
|
13525
|
+
}
|
|
13364
13526
|
function cleanupSessionRefs(repoRoot, sessionId) {
|
|
13365
13527
|
for (const prefix of [
|
|
13366
13528
|
`refs/hillclimb/baseline/${sessionId}`,
|
|
@@ -13388,20 +13550,24 @@ import fs11 from "fs";
|
|
|
13388
13550
|
import os7 from "os";
|
|
13389
13551
|
import path14 from "path";
|
|
13390
13552
|
var CURRENT_SCHEMA_VERSION = 3;
|
|
13391
|
-
var
|
|
13392
|
-
|
|
13393
|
-
|
|
13394
|
-
|
|
13553
|
+
var DEFAULT_STATE_DIR = path14.join(os7.homedir(), ".hillclimb", "git-traces");
|
|
13554
|
+
var LOCK_RETRIES = 120;
|
|
13555
|
+
var LOCK_RETRY_DELAY_MS = 500;
|
|
13556
|
+
function stateDir() {
|
|
13557
|
+
return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR;
|
|
13558
|
+
}
|
|
13559
|
+
function stateFileForRepo(repoRoot, tool, sessionId) {
|
|
13560
|
+
const hash = crypto.createHash("sha256").update(
|
|
13561
|
+
sessionId ? `${path14.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path14.resolve(repoRoot)}\0${tool}`
|
|
13562
|
+
).digest("hex").slice(0, 16);
|
|
13563
|
+
return path14.join(stateDir(), `${hash}.json`);
|
|
13395
13564
|
}
|
|
13396
13565
|
function lockFileForRepo(repoRoot, tool) {
|
|
13397
13566
|
return `${stateFileForRepo(repoRoot, tool)}.lock`;
|
|
13398
13567
|
}
|
|
13399
|
-
async function
|
|
13568
|
+
async function readStateFile(file) {
|
|
13400
13569
|
try {
|
|
13401
|
-
const raw = await fs11.promises.readFile(
|
|
13402
|
-
stateFileForRepo(repoRoot, tool),
|
|
13403
|
-
"utf-8"
|
|
13404
|
-
);
|
|
13570
|
+
const raw = await fs11.promises.readFile(file, "utf-8");
|
|
13405
13571
|
const parsed = JSON.parse(raw);
|
|
13406
13572
|
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) {
|
|
13407
13573
|
return null;
|
|
@@ -13411,24 +13577,112 @@ async function readSessionState(repoRoot, tool) {
|
|
|
13411
13577
|
return null;
|
|
13412
13578
|
}
|
|
13413
13579
|
}
|
|
13580
|
+
async function listScopedSessionStates(repoRoot, tool) {
|
|
13581
|
+
let entries;
|
|
13582
|
+
try {
|
|
13583
|
+
entries = await fs11.promises.readdir(stateDir(), { withFileTypes: true });
|
|
13584
|
+
} catch {
|
|
13585
|
+
return [];
|
|
13586
|
+
}
|
|
13587
|
+
const states = [];
|
|
13588
|
+
for (const entry of entries) {
|
|
13589
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
13590
|
+
const file = path14.join(stateDir(), entry.name);
|
|
13591
|
+
const state = await readStateFile(file);
|
|
13592
|
+
if (!state) continue;
|
|
13593
|
+
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
13594
|
+
continue;
|
|
13595
|
+
}
|
|
13596
|
+
if (path14.resolve(state.repoRoot) !== path14.resolve(repoRoot)) continue;
|
|
13597
|
+
if (path14.resolve(file) !== path14.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
|
|
13598
|
+
continue;
|
|
13599
|
+
}
|
|
13600
|
+
let mtimeMs = 0;
|
|
13601
|
+
try {
|
|
13602
|
+
mtimeMs = (await fs11.promises.stat(file)).mtimeMs;
|
|
13603
|
+
} catch {
|
|
13604
|
+
continue;
|
|
13605
|
+
}
|
|
13606
|
+
states.push({ state, mtimeMs });
|
|
13607
|
+
}
|
|
13608
|
+
return states;
|
|
13609
|
+
}
|
|
13610
|
+
async function listSessionStatesForSession(tool, sessionId) {
|
|
13611
|
+
let entries;
|
|
13612
|
+
try {
|
|
13613
|
+
entries = await fs11.promises.readdir(stateDir(), { withFileTypes: true });
|
|
13614
|
+
} catch {
|
|
13615
|
+
return [];
|
|
13616
|
+
}
|
|
13617
|
+
const states = [];
|
|
13618
|
+
for (const entry of entries) {
|
|
13619
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
13620
|
+
const file = path14.join(stateDir(), entry.name);
|
|
13621
|
+
const state = await readStateFile(file);
|
|
13622
|
+
if (!state) continue;
|
|
13623
|
+
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
13624
|
+
continue;
|
|
13625
|
+
}
|
|
13626
|
+
if (state.sessionId !== sessionId) continue;
|
|
13627
|
+
if (path14.resolve(file) !== path14.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
|
|
13628
|
+
continue;
|
|
13629
|
+
}
|
|
13630
|
+
let mtimeMs = 0;
|
|
13631
|
+
try {
|
|
13632
|
+
mtimeMs = (await fs11.promises.stat(file)).mtimeMs;
|
|
13633
|
+
} catch {
|
|
13634
|
+
continue;
|
|
13635
|
+
}
|
|
13636
|
+
states.push({ state, mtimeMs });
|
|
13637
|
+
}
|
|
13638
|
+
return states;
|
|
13639
|
+
}
|
|
13640
|
+
async function readSessionState(repoRoot, tool, sessionId) {
|
|
13641
|
+
if (sessionId) {
|
|
13642
|
+
const scoped = await readStateFile(
|
|
13643
|
+
stateFileForRepo(repoRoot, tool, sessionId)
|
|
13644
|
+
);
|
|
13645
|
+
if (scoped) return scoped;
|
|
13646
|
+
const legacy = await readStateFile(stateFileForRepo(repoRoot, tool));
|
|
13647
|
+
return legacy?.sessionId === sessionId ? legacy : null;
|
|
13648
|
+
}
|
|
13649
|
+
return readStateFile(stateFileForRepo(repoRoot, tool));
|
|
13650
|
+
}
|
|
13414
13651
|
async function writeSessionState(state, tool) {
|
|
13415
|
-
const file = stateFileForRepo(state.repoRoot, tool);
|
|
13416
|
-
await fs11.promises.mkdir(
|
|
13652
|
+
const file = stateFileForRepo(state.repoRoot, tool, state.sessionId);
|
|
13653
|
+
await fs11.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
|
|
13417
13654
|
const tmp = `${file}.tmp`;
|
|
13418
13655
|
await fs11.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
13419
13656
|
mode: 384
|
|
13420
13657
|
});
|
|
13421
13658
|
await fs11.promises.rename(tmp, file);
|
|
13659
|
+
const legacyFile = stateFileForRepo(state.repoRoot, tool);
|
|
13660
|
+
const legacy = await readStateFile(legacyFile);
|
|
13661
|
+
if (legacy?.sessionId === state.sessionId) {
|
|
13662
|
+
await deleteStateFile(legacyFile);
|
|
13663
|
+
}
|
|
13422
13664
|
}
|
|
13423
|
-
async function
|
|
13665
|
+
async function deleteStateFile(file) {
|
|
13424
13666
|
try {
|
|
13425
|
-
await fs11.promises.unlink(
|
|
13667
|
+
await fs11.promises.unlink(file);
|
|
13426
13668
|
} catch {
|
|
13427
13669
|
}
|
|
13428
13670
|
}
|
|
13429
|
-
async function
|
|
13671
|
+
async function deleteSessionState(repoRoot, tool, sessionId) {
|
|
13672
|
+
if (sessionId) {
|
|
13673
|
+
await deleteStateFile(stateFileForRepo(repoRoot, tool, sessionId));
|
|
13674
|
+
const legacyFile = stateFileForRepo(repoRoot, tool);
|
|
13675
|
+
const legacy = await readStateFile(legacyFile);
|
|
13676
|
+
if (legacy?.sessionId === sessionId) {
|
|
13677
|
+
await deleteStateFile(legacyFile);
|
|
13678
|
+
}
|
|
13679
|
+
return;
|
|
13680
|
+
}
|
|
13681
|
+
await deleteStateFile(stateFileForRepo(repoRoot, tool));
|
|
13682
|
+
}
|
|
13683
|
+
async function acquireLock(repoRoot, tool, retries = LOCK_RETRIES, delayMs = LOCK_RETRY_DELAY_MS) {
|
|
13430
13684
|
const lockPath = lockFileForRepo(repoRoot, tool);
|
|
13431
|
-
await fs11.promises.mkdir(
|
|
13685
|
+
await fs11.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
|
|
13432
13686
|
for (let i = 0; i < retries; i++) {
|
|
13433
13687
|
try {
|
|
13434
13688
|
const fd = await fs11.promises.open(
|
|
@@ -13459,6 +13713,7 @@ async function releaseLock(repoRoot, tool) {
|
|
|
13459
13713
|
var CLI_VERSION = "0.2.0";
|
|
13460
13714
|
var GIT_TRACES_SLUG = "git-traces";
|
|
13461
13715
|
var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
|
|
13716
|
+
var STALE_SCOPED_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
13462
13717
|
function formatEpochSeconds2(date) {
|
|
13463
13718
|
return String(Math.floor(date.getTime() / 1e3));
|
|
13464
13719
|
}
|
|
@@ -13469,6 +13724,16 @@ var TOOL_LABELS = {
|
|
|
13469
13724
|
"copilot-chat": "GitHub Copilot Chat",
|
|
13470
13725
|
opencode: "opencode"
|
|
13471
13726
|
};
|
|
13727
|
+
async function loadConfiguredRepos() {
|
|
13728
|
+
const file = await loadProjects();
|
|
13729
|
+
return Object.entries(file.projects).map(([repoRoot, config]) => ({
|
|
13730
|
+
repoRoot: path15.resolve(repoRoot),
|
|
13731
|
+
config
|
|
13732
|
+
})).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
|
|
13733
|
+
}
|
|
13734
|
+
function repoLabel(repoRoot) {
|
|
13735
|
+
return path15.basename(repoRoot) || repoRoot;
|
|
13736
|
+
}
|
|
13472
13737
|
function resolveCwd(payload) {
|
|
13473
13738
|
return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
|
|
13474
13739
|
}
|
|
@@ -13528,27 +13793,43 @@ function canUploadEpochBaselineArtifacts(epoch, artifacts) {
|
|
|
13528
13793
|
}
|
|
13529
13794
|
function pinEpochBaseline(repoRoot, sessionId, epoch) {
|
|
13530
13795
|
const baselineSha = captureBaselineSha(repoRoot);
|
|
13796
|
+
const baselineRefPrefix = `refs/hillclimb/baseline/${sessionId}/${epochPrefix(epoch)}`;
|
|
13797
|
+
deleteRef(repoRoot, baselineRefPrefix);
|
|
13798
|
+
pinRef(repoRoot, `${baselineRefPrefix}/tracked`, baselineSha);
|
|
13799
|
+
return { baselineSha, headSha: captureHeadSha(repoRoot) };
|
|
13800
|
+
}
|
|
13801
|
+
function pinFrozenBaselineTree(repoRoot, sessionId, epoch, baselineTreeSha) {
|
|
13802
|
+
const prefix = epochPrefix(epoch);
|
|
13803
|
+
const commit = execGit(repoRoot, [
|
|
13804
|
+
"commit-tree",
|
|
13805
|
+
baselineTreeSha,
|
|
13806
|
+
"-m",
|
|
13807
|
+
`frozen baseline ${prefix} for session ${sessionId}`
|
|
13808
|
+
]);
|
|
13531
13809
|
pinRef(
|
|
13532
13810
|
repoRoot,
|
|
13533
|
-
`refs/hillclimb/baseline/${sessionId}/${
|
|
13534
|
-
|
|
13811
|
+
`refs/hillclimb/baseline/${sessionId}/${prefix}/tree`,
|
|
13812
|
+
commit
|
|
13535
13813
|
);
|
|
13536
|
-
return { baselineSha, headSha: captureHeadSha(repoRoot) };
|
|
13537
13814
|
}
|
|
13538
|
-
function
|
|
13815
|
+
function freezeEpochBaseline(params) {
|
|
13539
13816
|
const {
|
|
13540
13817
|
repoRoot,
|
|
13541
13818
|
sessionId,
|
|
13542
13819
|
tool,
|
|
13543
13820
|
epoch,
|
|
13544
|
-
baselineSha,
|
|
13545
13821
|
prevHeadSha,
|
|
13546
13822
|
transitionKind,
|
|
13547
13823
|
startedAt
|
|
13548
13824
|
} = params;
|
|
13549
13825
|
const prefix = epochPrefix(epoch);
|
|
13550
13826
|
try {
|
|
13551
|
-
const
|
|
13827
|
+
const { baselineSha, headSha } = pinEpochBaseline(
|
|
13828
|
+
repoRoot,
|
|
13829
|
+
sessionId,
|
|
13830
|
+
epoch
|
|
13831
|
+
);
|
|
13832
|
+
const baselineMetadata = buildBaselineMetadata(
|
|
13552
13833
|
repoRoot,
|
|
13553
13834
|
sessionId,
|
|
13554
13835
|
tool,
|
|
@@ -13560,13 +13841,29 @@ function buildEpochBaselineArtifacts(params) {
|
|
|
13560
13841
|
startedAt
|
|
13561
13842
|
);
|
|
13562
13843
|
const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha);
|
|
13844
|
+
pinFrozenBaselineTree(repoRoot, sessionId, epoch, baselineTreeSha);
|
|
13845
|
+
return { baselineSha, baselineTreeSha, baselineMetadata, headSha };
|
|
13846
|
+
} catch (err) {
|
|
13847
|
+
appendLog(
|
|
13848
|
+
"error",
|
|
13849
|
+
`git-traces: failed to freeze baseline for ${prefix}: ${formatError(err)}`
|
|
13850
|
+
);
|
|
13851
|
+
return null;
|
|
13852
|
+
}
|
|
13853
|
+
}
|
|
13854
|
+
function buildFrozenEpochBaselineArtifacts(params) {
|
|
13855
|
+
const { repoRoot, sessionId, epoch, baselineTreeSha, baselineMetadata } = params;
|
|
13856
|
+
const prefix = epochPrefix(epoch);
|
|
13857
|
+
try {
|
|
13563
13858
|
const bundleBuffer = createBundleFromTree(
|
|
13564
13859
|
repoRoot,
|
|
13565
13860
|
baselineTreeSha,
|
|
13566
13861
|
`${sessionId}-${prefix}`,
|
|
13567
13862
|
`baseline ${prefix}`
|
|
13568
13863
|
);
|
|
13569
|
-
const metadataBuffer = Buffer.from(
|
|
13864
|
+
const metadataBuffer = Buffer.from(
|
|
13865
|
+
JSON.stringify(baselineMetadata, null, 2)
|
|
13866
|
+
);
|
|
13570
13867
|
return { baselineTreeSha, metadataBuffer, bundleBuffer };
|
|
13571
13868
|
} catch (err) {
|
|
13572
13869
|
appendLog(
|
|
@@ -13576,6 +13873,46 @@ function buildEpochBaselineArtifacts(params) {
|
|
|
13576
13873
|
return null;
|
|
13577
13874
|
}
|
|
13578
13875
|
}
|
|
13876
|
+
function buildEpochBaselineArtifacts(params) {
|
|
13877
|
+
const {
|
|
13878
|
+
repoRoot,
|
|
13879
|
+
sessionId,
|
|
13880
|
+
tool,
|
|
13881
|
+
epoch,
|
|
13882
|
+
baselineSha,
|
|
13883
|
+
prevHeadSha,
|
|
13884
|
+
transitionKind,
|
|
13885
|
+
startedAt
|
|
13886
|
+
} = params;
|
|
13887
|
+
const prefix = epochPrefix(epoch);
|
|
13888
|
+
try {
|
|
13889
|
+
const metadata = buildBaselineMetadata(
|
|
13890
|
+
repoRoot,
|
|
13891
|
+
sessionId,
|
|
13892
|
+
tool,
|
|
13893
|
+
baselineSha,
|
|
13894
|
+
CLI_VERSION,
|
|
13895
|
+
epoch,
|
|
13896
|
+
prevHeadSha,
|
|
13897
|
+
transitionKind,
|
|
13898
|
+
startedAt
|
|
13899
|
+
);
|
|
13900
|
+
const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha);
|
|
13901
|
+
return buildFrozenEpochBaselineArtifacts({
|
|
13902
|
+
repoRoot,
|
|
13903
|
+
sessionId,
|
|
13904
|
+
epoch,
|
|
13905
|
+
baselineTreeSha,
|
|
13906
|
+
baselineMetadata: metadata
|
|
13907
|
+
});
|
|
13908
|
+
} catch (err) {
|
|
13909
|
+
appendLog(
|
|
13910
|
+
"error",
|
|
13911
|
+
`git-traces: failed to build baseline artifacts for ${prefix}: ${formatError(err)}`
|
|
13912
|
+
);
|
|
13913
|
+
return null;
|
|
13914
|
+
}
|
|
13915
|
+
}
|
|
13579
13916
|
async function uploadEpochBaselineArtifacts(params) {
|
|
13580
13917
|
const { client, contributionId, epoch, artifacts } = params;
|
|
13581
13918
|
const prefix = epochPrefix(epoch);
|
|
@@ -13596,6 +13933,34 @@ async function uploadEpochBaselineArtifacts(params) {
|
|
|
13596
13933
|
artifacts.metadataBuffer
|
|
13597
13934
|
);
|
|
13598
13935
|
}
|
|
13936
|
+
async function createGitTracesContribution(params) {
|
|
13937
|
+
const { client, config, repoRoot, state, tool, now, artifacts } = params;
|
|
13938
|
+
const toolLabel = TOOL_LABELS[tool] ?? "Claude";
|
|
13939
|
+
const epochSeconds = formatEpochSeconds2(now);
|
|
13940
|
+
const shortId = state.sessionId.slice(0, 12);
|
|
13941
|
+
const repoName = repoLabel(repoRoot);
|
|
13942
|
+
const contribution = await client.createContribution(config.projectId, {
|
|
13943
|
+
contributionTypeSlug: GIT_TRACES_SLUG,
|
|
13944
|
+
title: `${toolLabel} session ${shortId} \u2014 ${repoName} \u2014 ${epochSeconds}`,
|
|
13945
|
+
body: `Session ID: ${state.sessionId}
|
|
13946
|
+
Tool: ${toolLabel}
|
|
13947
|
+
Repo: ${repoRoot}
|
|
13948
|
+
Uploaded: ${now.toISOString()}`
|
|
13949
|
+
});
|
|
13950
|
+
const uploaded = await uploadEpochBaselineArtifacts({
|
|
13951
|
+
client,
|
|
13952
|
+
contributionId: contribution.id,
|
|
13953
|
+
epoch: 1,
|
|
13954
|
+
artifacts
|
|
13955
|
+
});
|
|
13956
|
+
if (!uploaded) return null;
|
|
13957
|
+
await client.submitContribution(contribution.id);
|
|
13958
|
+
appendLog(
|
|
13959
|
+
"info",
|
|
13960
|
+
`git-traces: baseline uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contribution.id}, epoch=1, bundleBytes=${artifacts.bundleBuffer.byteLength}, metadataBytes=${artifacts.metadataBuffer.byteLength})`
|
|
13961
|
+
);
|
|
13962
|
+
return contribution.id;
|
|
13963
|
+
}
|
|
13599
13964
|
async function uploadEpochBaseline(params) {
|
|
13600
13965
|
const artifacts = buildEpochBaselineArtifacts(params);
|
|
13601
13966
|
if (!artifacts) return null;
|
|
@@ -13619,45 +13984,115 @@ async function openEpoch(params) {
|
|
|
13619
13984
|
if (!uploaded) return null;
|
|
13620
13985
|
return { baselineSha, baselineTreeSha: uploaded.baselineTreeSha, headSha };
|
|
13621
13986
|
}
|
|
13622
|
-
async function initializeSession(
|
|
13623
|
-
const
|
|
13624
|
-
|
|
13625
|
-
|
|
13626
|
-
|
|
13627
|
-
|
|
13628
|
-
|
|
13629
|
-
|
|
13630
|
-
|
|
13631
|
-
|
|
13987
|
+
async function initializeSession(repoRoot, tool, sessionId) {
|
|
13988
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13989
|
+
const frozen = freezeEpochBaseline({
|
|
13990
|
+
repoRoot,
|
|
13991
|
+
sessionId,
|
|
13992
|
+
tool,
|
|
13993
|
+
epoch: 1,
|
|
13994
|
+
prevHeadSha: null,
|
|
13995
|
+
transitionKind: "initial",
|
|
13996
|
+
startedAt
|
|
13997
|
+
});
|
|
13998
|
+
if (!frozen) return null;
|
|
13632
13999
|
const state = {
|
|
13633
14000
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
13634
14001
|
sessionId,
|
|
13635
14002
|
contributionId: null,
|
|
13636
|
-
baselineSha,
|
|
13637
|
-
baselineTreeSha:
|
|
13638
|
-
|
|
13639
|
-
|
|
13640
|
-
|
|
14003
|
+
baselineSha: frozen.baselineSha,
|
|
14004
|
+
baselineTreeSha: frozen.baselineTreeSha,
|
|
14005
|
+
baselineMetadata: frozen.baselineMetadata,
|
|
14006
|
+
lastSnapshotSha: frozen.baselineSha,
|
|
14007
|
+
lastSnapshotTreeSha: frozen.baselineTreeSha,
|
|
14008
|
+
startedAt,
|
|
13641
14009
|
epoch: 1,
|
|
13642
14010
|
turnCount: 0,
|
|
13643
|
-
headSha,
|
|
13644
|
-
repoRoot
|
|
14011
|
+
headSha: frozen.headSha,
|
|
14012
|
+
repoRoot
|
|
13645
14013
|
};
|
|
13646
14014
|
await writeSessionState(state, tool);
|
|
13647
14015
|
appendLog(
|
|
13648
14016
|
"info",
|
|
13649
|
-
`git-traces: session pending (baseline=${baselineSha.slice(0, 8)}, awaiting first turn)`
|
|
14017
|
+
`git-traces: session pending (repo=${repoRoot}, baseline=${frozen.baselineSha.slice(0, 8)}, awaiting first turn)`
|
|
13650
14018
|
);
|
|
13651
14019
|
return state;
|
|
13652
14020
|
}
|
|
14021
|
+
async function cleanupStaleScopedSessionStates(repoRoot, tool, currentSessionId, options = {}) {
|
|
14022
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
14023
|
+
const ttlMs = options.ttlMs ?? STALE_SCOPED_STATE_TTL_MS;
|
|
14024
|
+
const states = await listScopedSessionStates(repoRoot, tool);
|
|
14025
|
+
let removed = 0;
|
|
14026
|
+
for (const { state, mtimeMs } of states) {
|
|
14027
|
+
if (state.sessionId === currentSessionId) continue;
|
|
14028
|
+
const startedAtMs = Date.parse(state.startedAt);
|
|
14029
|
+
const ageBaseMs = Number.isNaN(startedAtMs) ? mtimeMs : startedAtMs;
|
|
14030
|
+
if (nowMs - ageBaseMs <= ttlMs) continue;
|
|
14031
|
+
appendLog(
|
|
14032
|
+
"info",
|
|
14033
|
+
`git-traces: cleaning up stale scoped session ${state.sessionId}`
|
|
14034
|
+
);
|
|
14035
|
+
cleanupSessionRefs(repoRoot, state.sessionId);
|
|
14036
|
+
await deleteSessionState(repoRoot, tool, state.sessionId);
|
|
14037
|
+
removed++;
|
|
14038
|
+
}
|
|
14039
|
+
return removed;
|
|
14040
|
+
}
|
|
14041
|
+
async function processSessionStartRepo(repo, tool, sessionId) {
|
|
14042
|
+
const { repoRoot } = repo;
|
|
14043
|
+
if (!isGitRepo(repoRoot)) {
|
|
14044
|
+
appendLog(
|
|
14045
|
+
"info",
|
|
14046
|
+
`git-traces: skipping repo on SessionStart (repo=${repoRoot}, reason=not-git-repo)`
|
|
14047
|
+
);
|
|
14048
|
+
return "skipped";
|
|
14049
|
+
}
|
|
14050
|
+
await acquireLock(repoRoot, tool);
|
|
14051
|
+
try {
|
|
14052
|
+
const staleLegacy = await readSessionState(repoRoot, tool);
|
|
14053
|
+
if (staleLegacy && staleLegacy.sessionId !== sessionId) {
|
|
14054
|
+
appendLog(
|
|
14055
|
+
"info",
|
|
14056
|
+
`git-traces: cleaning up stale session ${staleLegacy.sessionId} (repo=${repoRoot})`
|
|
14057
|
+
);
|
|
14058
|
+
cleanupSessionRefs(repoRoot, staleLegacy.sessionId);
|
|
14059
|
+
await deleteSessionState(repoRoot, tool);
|
|
14060
|
+
}
|
|
14061
|
+
const staleCount = await cleanupStaleScopedSessionStates(
|
|
14062
|
+
repoRoot,
|
|
14063
|
+
tool,
|
|
14064
|
+
sessionId
|
|
14065
|
+
);
|
|
14066
|
+
if (staleCount > 0) {
|
|
14067
|
+
appendLog(
|
|
14068
|
+
"info",
|
|
14069
|
+
`git-traces: cleaned stale scoped sessions (repo=${repoRoot}, count=${staleCount})`
|
|
14070
|
+
);
|
|
14071
|
+
}
|
|
14072
|
+
const state = await initializeSession(repoRoot, tool, sessionId);
|
|
14073
|
+
return state ? "initialized" : "failed";
|
|
14074
|
+
} catch (err) {
|
|
14075
|
+
appendLog(
|
|
14076
|
+
"error",
|
|
14077
|
+
`git-traces: SessionStart failed for repo ${repoRoot}: ${formatError(err)}`
|
|
14078
|
+
);
|
|
14079
|
+
return "failed";
|
|
14080
|
+
} finally {
|
|
14081
|
+
await releaseLock(repoRoot, tool);
|
|
14082
|
+
}
|
|
14083
|
+
}
|
|
13653
14084
|
async function handleSessionStart(payload, tool) {
|
|
13654
14085
|
const cwd = resolveCwd(payload);
|
|
13655
14086
|
if (!cwd) {
|
|
13656
14087
|
appendLog("warn", "git-traces: no cwd in payload, skipping");
|
|
13657
14088
|
return;
|
|
13658
14089
|
}
|
|
13659
|
-
|
|
13660
|
-
|
|
14090
|
+
const startingProject = await findProjectForCwd(cwd);
|
|
14091
|
+
if (!startingProject) {
|
|
14092
|
+
appendLog(
|
|
14093
|
+
"warn",
|
|
14094
|
+
"git-traces: no hillclimb project config found, skipping"
|
|
14095
|
+
);
|
|
13661
14096
|
return;
|
|
13662
14097
|
}
|
|
13663
14098
|
const sessionId = resolveSessionId(payload);
|
|
@@ -13665,115 +14100,146 @@ async function handleSessionStart(payload, tool) {
|
|
|
13665
14100
|
appendLog("warn", "git-traces: no session_id in payload, skipping");
|
|
13666
14101
|
return;
|
|
13667
14102
|
}
|
|
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);
|
|
14103
|
+
const repos = await loadConfiguredRepos();
|
|
14104
|
+
let initialized = 0;
|
|
14105
|
+
let skipped = 0;
|
|
14106
|
+
let failed = 0;
|
|
14107
|
+
for (const repo of repos) {
|
|
14108
|
+
const outcome = await processSessionStartRepo(repo, tool, sessionId);
|
|
14109
|
+
if (outcome === "initialized") initialized++;
|
|
14110
|
+
else if (outcome === "skipped") skipped++;
|
|
14111
|
+
else failed++;
|
|
13682
14112
|
}
|
|
14113
|
+
appendLog(
|
|
14114
|
+
"info",
|
|
14115
|
+
`git-traces: SessionStart summary (session=${sessionId}, tool=${tool}, triggerRepo=${startingProject.repoRoot}, configured=${repos.length}, initialized=${initialized}, skipped=${skipped}, failed=${failed})`
|
|
14116
|
+
);
|
|
13683
14117
|
}
|
|
13684
|
-
|
|
13685
|
-
|
|
13686
|
-
|
|
13687
|
-
|
|
13688
|
-
|
|
13689
|
-
|
|
13690
|
-
|
|
13691
|
-
|
|
13692
|
-
|
|
13693
|
-
|
|
13694
|
-
|
|
13695
|
-
|
|
13696
|
-
|
|
13697
|
-
|
|
13698
|
-
|
|
13699
|
-
|
|
13700
|
-
|
|
13701
|
-
|
|
13702
|
-
|
|
13703
|
-
|
|
13704
|
-
|
|
13705
|
-
|
|
14118
|
+
function buildInitialBaselineArtifactsForState(repoRoot, tool, state) {
|
|
14119
|
+
if (state.baselineTreeSha && state.baselineMetadata) {
|
|
14120
|
+
return buildFrozenEpochBaselineArtifacts({
|
|
14121
|
+
repoRoot,
|
|
14122
|
+
sessionId: state.sessionId,
|
|
14123
|
+
epoch: 1,
|
|
14124
|
+
baselineTreeSha: state.baselineTreeSha,
|
|
14125
|
+
baselineMetadata: state.baselineMetadata
|
|
14126
|
+
});
|
|
14127
|
+
}
|
|
14128
|
+
return buildEpochBaselineArtifacts({
|
|
14129
|
+
repoRoot,
|
|
14130
|
+
sessionId: state.sessionId,
|
|
14131
|
+
tool,
|
|
14132
|
+
epoch: 1,
|
|
14133
|
+
baselineSha: state.baselineSha,
|
|
14134
|
+
prevHeadSha: null,
|
|
14135
|
+
transitionKind: "initial",
|
|
14136
|
+
startedAt: state.startedAt
|
|
14137
|
+
});
|
|
14138
|
+
}
|
|
14139
|
+
async function loadRepoClient(repo) {
|
|
14140
|
+
const identity = await loadIdentity(repo.config.apiBaseUrl);
|
|
14141
|
+
if (!identity) {
|
|
14142
|
+
appendLog(
|
|
14143
|
+
"warn",
|
|
14144
|
+
`git-traces: skipping repo on Stop (repo=${repo.repoRoot}, project=${repo.config.projectId}, reason=no-saved-login, apiBaseUrl=${repo.config.apiBaseUrl})`
|
|
13706
14145
|
);
|
|
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
|
-
await writeSessionState(state, tool);
|
|
14146
|
+
return null;
|
|
14147
|
+
}
|
|
14148
|
+
return new PlatformClient(repo.config.apiBaseUrl, identity.sessionCookie);
|
|
14149
|
+
}
|
|
14150
|
+
async function registerInitialContribution(params) {
|
|
14151
|
+
const { repo, state, tool, client, artifacts } = params;
|
|
14152
|
+
const contributionId = await createGitTracesContribution({
|
|
14153
|
+
client,
|
|
14154
|
+
config: repo.config,
|
|
14155
|
+
repoRoot: repo.repoRoot,
|
|
14156
|
+
state,
|
|
14157
|
+
tool,
|
|
14158
|
+
now: /* @__PURE__ */ new Date(),
|
|
14159
|
+
artifacts
|
|
14160
|
+
});
|
|
14161
|
+
if (!contributionId) return false;
|
|
14162
|
+
state.contributionId = contributionId;
|
|
14163
|
+
state.baselineTreeSha = artifacts.baselineTreeSha;
|
|
14164
|
+
state.lastSnapshotTreeSha = artifacts.baselineTreeSha;
|
|
14165
|
+
await writeSessionState(state, tool);
|
|
14166
|
+
appendLog(
|
|
14167
|
+
"info",
|
|
14168
|
+
`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)})`
|
|
14169
|
+
);
|
|
14170
|
+
return true;
|
|
14171
|
+
}
|
|
14172
|
+
async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
14173
|
+
const { repoRoot, config } = repo;
|
|
14174
|
+
if (!isGitRepo(repoRoot)) {
|
|
14175
|
+
appendLog(
|
|
14176
|
+
"info",
|
|
14177
|
+
`git-traces: skipping repo on Stop (repo=${repoRoot}, project=${config.projectId}, reason=not-git-repo)`
|
|
14178
|
+
);
|
|
14179
|
+
return "skipped";
|
|
14180
|
+
}
|
|
14181
|
+
await acquireLock(repoRoot, tool);
|
|
14182
|
+
try {
|
|
14183
|
+
const state = await readSessionState(repoRoot, tool, sessionId);
|
|
14184
|
+
if (!state) {
|
|
13747
14185
|
appendLog(
|
|
13748
14186
|
"info",
|
|
13749
|
-
`git-traces:
|
|
14187
|
+
`git-traces: skipping repo on Stop (repo=${repoRoot}, project=${config.projectId}, reason=no-active-state)`
|
|
13750
14188
|
);
|
|
14189
|
+
return "no-state";
|
|
13751
14190
|
}
|
|
13752
|
-
const
|
|
13753
|
-
|
|
13754
|
-
if (contributionId === null || lastSnapshotTreeSha === null) {
|
|
14191
|
+
const lastSnapshotTreeSha = state.lastSnapshotTreeSha ?? state.baselineTreeSha;
|
|
14192
|
+
if (!lastSnapshotTreeSha) {
|
|
13755
14193
|
appendLog(
|
|
13756
14194
|
"error",
|
|
13757
|
-
|
|
14195
|
+
`git-traces: invariant violation \u2014 session state missing snapshot tree SHA (repo=${repoRoot}, session=${state.sessionId})`
|
|
13758
14196
|
);
|
|
13759
|
-
return;
|
|
14197
|
+
return "failed";
|
|
13760
14198
|
}
|
|
13761
|
-
const currentHeadSha = captureHeadSha(
|
|
14199
|
+
const currentHeadSha = captureHeadSha(repoRoot);
|
|
13762
14200
|
if (currentHeadSha !== state.headSha) {
|
|
14201
|
+
const client2 = await loadRepoClient(repo);
|
|
14202
|
+
if (!client2) return "skipped";
|
|
14203
|
+
if (state.contributionId === null) {
|
|
14204
|
+
const artifacts2 = buildInitialBaselineArtifactsForState(
|
|
14205
|
+
repoRoot,
|
|
14206
|
+
tool,
|
|
14207
|
+
state
|
|
14208
|
+
);
|
|
14209
|
+
if (!artifacts2 || !canUploadEpochBaselineArtifacts(1, artifacts2)) {
|
|
14210
|
+
return "skipped";
|
|
14211
|
+
}
|
|
14212
|
+
const registered = await registerInitialContribution({
|
|
14213
|
+
repo,
|
|
14214
|
+
state,
|
|
14215
|
+
tool,
|
|
14216
|
+
client: client2,
|
|
14217
|
+
artifacts: artifacts2
|
|
14218
|
+
});
|
|
14219
|
+
if (!registered) return "failed";
|
|
14220
|
+
}
|
|
14221
|
+
const contributionId2 = state.contributionId;
|
|
14222
|
+
if (contributionId2 === null) {
|
|
14223
|
+
appendLog(
|
|
14224
|
+
"error",
|
|
14225
|
+
`git-traces: invariant violation \u2014 missing contribution after registration (repo=${repoRoot}, session=${state.sessionId})`
|
|
14226
|
+
);
|
|
14227
|
+
return "failed";
|
|
14228
|
+
}
|
|
13763
14229
|
const transitionKind = detectTransitionKind(
|
|
13764
|
-
|
|
14230
|
+
repoRoot,
|
|
13765
14231
|
state.headSha || null,
|
|
13766
14232
|
currentHeadSha
|
|
13767
14233
|
);
|
|
13768
14234
|
const nextEpoch = state.epoch + 1;
|
|
13769
14235
|
appendLog(
|
|
13770
14236
|
"info",
|
|
13771
|
-
`git-traces: HEAD moved (
|
|
14237
|
+
`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
14238
|
);
|
|
13773
14239
|
const artifacts = await openEpoch({
|
|
13774
|
-
repoRoot
|
|
13775
|
-
client,
|
|
13776
|
-
contributionId,
|
|
14240
|
+
repoRoot,
|
|
14241
|
+
client: client2,
|
|
14242
|
+
contributionId: contributionId2,
|
|
13777
14243
|
sessionId: state.sessionId,
|
|
13778
14244
|
tool,
|
|
13779
14245
|
epoch: nextEpoch,
|
|
@@ -13781,7 +14247,7 @@ Uploaded: ${now.toISOString()}`
|
|
|
13781
14247
|
transitionKind,
|
|
13782
14248
|
startedAt: state.startedAt
|
|
13783
14249
|
});
|
|
13784
|
-
if (!artifacts) return;
|
|
14250
|
+
if (!artifacts) return "failed";
|
|
13785
14251
|
const next = {
|
|
13786
14252
|
...state,
|
|
13787
14253
|
baselineSha: artifacts.baselineSha,
|
|
@@ -13793,66 +14259,204 @@ Uploaded: ${now.toISOString()}`
|
|
|
13793
14259
|
headSha: artifacts.headSha
|
|
13794
14260
|
};
|
|
13795
14261
|
await writeSessionState(next, tool);
|
|
13796
|
-
|
|
14262
|
+
appendLog(
|
|
14263
|
+
"info",
|
|
14264
|
+
`git-traces: epoch baseline uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId2}, epoch=${nextEpoch}, baseline=${artifacts.baselineSha.slice(0, 8)})`
|
|
14265
|
+
);
|
|
14266
|
+
return "uploaded";
|
|
13797
14267
|
}
|
|
13798
|
-
const currentSha = captureSnapshotSha(
|
|
13799
|
-
const currentTreeSha = buildSnapshotTree(
|
|
14268
|
+
const currentSha = captureSnapshotSha(repoRoot);
|
|
14269
|
+
const currentTreeSha = buildSnapshotTree(repoRoot, currentSha);
|
|
13800
14270
|
const patchBuffer = createTreeDiffPatchGz(
|
|
13801
|
-
|
|
14271
|
+
repoRoot,
|
|
13802
14272
|
lastSnapshotTreeSha,
|
|
13803
14273
|
currentTreeSha
|
|
13804
14274
|
);
|
|
13805
14275
|
if (!patchBuffer) {
|
|
13806
14276
|
appendLog(
|
|
13807
14277
|
"info",
|
|
13808
|
-
|
|
14278
|
+
`git-traces: turn produced identical snapshot tree, skipping upload (repo=${repoRoot}, project=${config.projectId})`
|
|
13809
14279
|
);
|
|
13810
|
-
return;
|
|
14280
|
+
return "unchanged";
|
|
13811
14281
|
}
|
|
13812
|
-
state.turnCount++;
|
|
13813
14282
|
const prefix = epochPrefix(state.epoch);
|
|
13814
|
-
const
|
|
14283
|
+
const nextTurnCount = state.turnCount + 1;
|
|
14284
|
+
const turnLabel = turnSuffix(nextTurnCount);
|
|
14285
|
+
const filename = `${prefix}-${turnLabel}-${recordedAt}.patch.gz`;
|
|
14286
|
+
if (state.contributionId === null && !canUploadFile(filename, patchBuffer)) {
|
|
14287
|
+
appendLog(
|
|
14288
|
+
"warn",
|
|
14289
|
+
`git-traces: first changed turn skipped before contribution creation (repo=${repoRoot}, project=${config.projectId}, reason=patch-too-large)`
|
|
14290
|
+
);
|
|
14291
|
+
return "skipped";
|
|
14292
|
+
}
|
|
14293
|
+
const client = await loadRepoClient(repo);
|
|
14294
|
+
if (!client) return "skipped";
|
|
14295
|
+
if (state.contributionId === null) {
|
|
14296
|
+
const artifacts = buildInitialBaselineArtifactsForState(
|
|
14297
|
+
repoRoot,
|
|
14298
|
+
tool,
|
|
14299
|
+
state
|
|
14300
|
+
);
|
|
14301
|
+
if (!artifacts || !canUploadEpochBaselineArtifacts(1, artifacts)) {
|
|
14302
|
+
return "skipped";
|
|
14303
|
+
}
|
|
14304
|
+
const registered = await registerInitialContribution({
|
|
14305
|
+
repo,
|
|
14306
|
+
state,
|
|
14307
|
+
tool,
|
|
14308
|
+
client,
|
|
14309
|
+
artifacts
|
|
14310
|
+
});
|
|
14311
|
+
if (!registered) return "failed";
|
|
14312
|
+
}
|
|
14313
|
+
const contributionId = state.contributionId;
|
|
14314
|
+
if (contributionId === null) {
|
|
14315
|
+
appendLog(
|
|
14316
|
+
"error",
|
|
14317
|
+
`git-traces: invariant violation \u2014 missing contribution before patch upload (repo=${repoRoot}, session=${state.sessionId})`
|
|
14318
|
+
);
|
|
14319
|
+
return "failed";
|
|
14320
|
+
}
|
|
13815
14321
|
pinRef(
|
|
13816
|
-
|
|
14322
|
+
repoRoot,
|
|
13817
14323
|
`refs/hillclimb/turns/${state.sessionId}/${prefix}/${turnLabel}`,
|
|
13818
14324
|
currentSha
|
|
13819
14325
|
);
|
|
13820
|
-
const
|
|
13821
|
-
await uploadFile(
|
|
14326
|
+
const uploaded = await uploadFile(
|
|
13822
14327
|
client,
|
|
13823
14328
|
contributionId,
|
|
13824
14329
|
filename,
|
|
13825
14330
|
"application/gzip",
|
|
13826
14331
|
patchBuffer
|
|
13827
14332
|
);
|
|
14333
|
+
if (!uploaded) {
|
|
14334
|
+
appendLog(
|
|
14335
|
+
"warn",
|
|
14336
|
+
`git-traces: ${prefix}-${turnLabel} not uploaded; keeping last uploaded snapshot unchanged (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId})`
|
|
14337
|
+
);
|
|
14338
|
+
return "skipped";
|
|
14339
|
+
}
|
|
14340
|
+
state.turnCount = nextTurnCount;
|
|
13828
14341
|
state.lastSnapshotSha = currentSha;
|
|
13829
14342
|
state.lastSnapshotTreeSha = currentTreeSha;
|
|
13830
14343
|
await writeSessionState(state, tool);
|
|
13831
14344
|
appendLog(
|
|
13832
14345
|
"info",
|
|
13833
|
-
`git-traces:
|
|
14346
|
+
`git-traces: patch uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId}, epoch=${state.epoch}, turn=${turnLabel}, bytes=${patchBuffer.byteLength})`
|
|
13834
14347
|
);
|
|
14348
|
+
return "uploaded";
|
|
14349
|
+
} catch (err) {
|
|
14350
|
+
appendLog(
|
|
14351
|
+
"error",
|
|
14352
|
+
`git-traces: Stop failed for repo ${repoRoot}: ${formatError(err)}`
|
|
14353
|
+
);
|
|
14354
|
+
return "failed";
|
|
13835
14355
|
} finally {
|
|
13836
|
-
await releaseLock(
|
|
14356
|
+
await releaseLock(repoRoot, tool);
|
|
13837
14357
|
}
|
|
13838
14358
|
}
|
|
13839
|
-
async function
|
|
14359
|
+
async function handleStop(payload, tool) {
|
|
13840
14360
|
const cwd = resolveCwd(payload);
|
|
13841
14361
|
if (!cwd) return;
|
|
13842
|
-
await
|
|
14362
|
+
const project = await findProjectForCwd(cwd);
|
|
14363
|
+
if (!project) return;
|
|
14364
|
+
const sessionId = resolveSessionId(payload);
|
|
14365
|
+
const recordedAt = Date.now();
|
|
14366
|
+
const repos = await loadConfiguredRepos();
|
|
14367
|
+
const repoByRoot = new Map(repos.map((repo) => [repo.repoRoot, repo]));
|
|
14368
|
+
const targets = [];
|
|
14369
|
+
let missingConfig = 0;
|
|
14370
|
+
if (sessionId) {
|
|
14371
|
+
const storedStates = await listSessionStatesForSession(tool, sessionId);
|
|
14372
|
+
for (const { state } of storedStates) {
|
|
14373
|
+
const repo = repoByRoot.get(path15.resolve(state.repoRoot));
|
|
14374
|
+
if (!repo) {
|
|
14375
|
+
missingConfig++;
|
|
14376
|
+
appendLog(
|
|
14377
|
+
"warn",
|
|
14378
|
+
`git-traces: skipping repo on Stop (repo=${state.repoRoot}, session=${sessionId}, reason=missing-config)`
|
|
14379
|
+
);
|
|
14380
|
+
continue;
|
|
14381
|
+
}
|
|
14382
|
+
targets.push(repo);
|
|
14383
|
+
}
|
|
14384
|
+
if (targets.length === 0 && missingConfig === 0) {
|
|
14385
|
+
targets.push({ repoRoot: project.repoRoot, config: project.config });
|
|
14386
|
+
}
|
|
14387
|
+
} else {
|
|
14388
|
+
targets.push({ repoRoot: project.repoRoot, config: project.config });
|
|
14389
|
+
}
|
|
14390
|
+
let uploaded = 0;
|
|
14391
|
+
let unchanged = 0;
|
|
14392
|
+
let skipped = missingConfig;
|
|
14393
|
+
let noState = 0;
|
|
14394
|
+
let failed = 0;
|
|
14395
|
+
for (const repo of targets) {
|
|
14396
|
+
const outcome = await processStopRepo(repo, tool, sessionId, recordedAt);
|
|
14397
|
+
if (outcome === "uploaded") uploaded++;
|
|
14398
|
+
else if (outcome === "unchanged") unchanged++;
|
|
14399
|
+
else if (outcome === "skipped") skipped++;
|
|
14400
|
+
else if (outcome === "no-state") noState++;
|
|
14401
|
+
else failed++;
|
|
14402
|
+
}
|
|
14403
|
+
appendLog(
|
|
14404
|
+
"info",
|
|
14405
|
+
`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})`
|
|
14406
|
+
);
|
|
14407
|
+
}
|
|
14408
|
+
async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
|
|
14409
|
+
await acquireLock(repoRoot, tool);
|
|
13843
14410
|
try {
|
|
13844
|
-
const state = await readSessionState(
|
|
13845
|
-
if (!state) return;
|
|
13846
|
-
cleanupSessionRefs(
|
|
13847
|
-
await deleteSessionState(
|
|
14411
|
+
const state = await readSessionState(repoRoot, tool, sessionId);
|
|
14412
|
+
if (!state) return "no-state";
|
|
14413
|
+
cleanupSessionRefs(repoRoot, state.sessionId);
|
|
14414
|
+
await deleteSessionState(repoRoot, tool, state.sessionId);
|
|
13848
14415
|
appendLog(
|
|
13849
14416
|
"info",
|
|
13850
|
-
`git-traces: session ${state.sessionId} cleaned up (
|
|
14417
|
+
`git-traces: session ${state.sessionId} cleaned up (repo=${repoRoot}, epochCount=${state.epoch}, lastTurnCount=${state.turnCount})`
|
|
14418
|
+
);
|
|
14419
|
+
return "cleaned";
|
|
14420
|
+
} catch (err) {
|
|
14421
|
+
appendLog(
|
|
14422
|
+
"error",
|
|
14423
|
+
`git-traces: SessionEnd cleanup failed for repo ${repoRoot}: ${formatError(err)}`
|
|
13851
14424
|
);
|
|
14425
|
+
return "failed";
|
|
13852
14426
|
} finally {
|
|
13853
|
-
await releaseLock(
|
|
14427
|
+
await releaseLock(repoRoot, tool);
|
|
13854
14428
|
}
|
|
13855
14429
|
}
|
|
14430
|
+
async function handleSessionEnd(payload, tool) {
|
|
14431
|
+
const cwd = resolveCwd(payload);
|
|
14432
|
+
const sessionId = resolveSessionId(payload);
|
|
14433
|
+
const project = cwd ? await findProjectForCwd(cwd) : null;
|
|
14434
|
+
const triggerRepo = project?.repoRoot ?? cwd ?? "<none>";
|
|
14435
|
+
const repoRoots = [];
|
|
14436
|
+
if (sessionId) {
|
|
14437
|
+
const states = await listSessionStatesForSession(tool, sessionId);
|
|
14438
|
+
for (const { state } of states) {
|
|
14439
|
+
repoRoots.push(path15.resolve(state.repoRoot));
|
|
14440
|
+
}
|
|
14441
|
+
}
|
|
14442
|
+
if (repoRoots.length === 0 && cwd) {
|
|
14443
|
+
repoRoots.push(project?.repoRoot ?? cwd);
|
|
14444
|
+
}
|
|
14445
|
+
if (repoRoots.length === 0) return;
|
|
14446
|
+
let cleaned = 0;
|
|
14447
|
+
let noState = 0;
|
|
14448
|
+
let failed = 0;
|
|
14449
|
+
for (const repoRoot of repoRoots) {
|
|
14450
|
+
const outcome = await cleanupSessionStateForRepo(repoRoot, tool, sessionId);
|
|
14451
|
+
if (outcome === "cleaned") cleaned++;
|
|
14452
|
+
else if (outcome === "no-state") noState++;
|
|
14453
|
+
else failed++;
|
|
14454
|
+
}
|
|
14455
|
+
appendLog(
|
|
14456
|
+
"info",
|
|
14457
|
+
`git-traces: SessionEnd summary (session=${sessionId ?? "<none>"}, tool=${tool}, triggerRepo=${triggerRepo}, states=${repoRoots.length}, cleaned=${cleaned}, noState=${noState}, failed=${failed})`
|
|
14458
|
+
);
|
|
14459
|
+
}
|
|
13856
14460
|
|
|
13857
14461
|
// src/git-traces/index.ts
|
|
13858
14462
|
var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
|
|
@@ -13868,6 +14472,25 @@ var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
|
13868
14472
|
"cursor",
|
|
13869
14473
|
"opencode"
|
|
13870
14474
|
]);
|
|
14475
|
+
function classifyHookEvent(event) {
|
|
14476
|
+
switch (event) {
|
|
14477
|
+
case "SessionStart":
|
|
14478
|
+
case "sessionStart":
|
|
14479
|
+
case "session.created":
|
|
14480
|
+
return "sessionStart";
|
|
14481
|
+
case "Stop":
|
|
14482
|
+
case "stop":
|
|
14483
|
+
case "session.idle":
|
|
14484
|
+
return "stop";
|
|
14485
|
+
case "SessionEnd":
|
|
14486
|
+
case "sessionEnd":
|
|
14487
|
+
case "session.deleted":
|
|
14488
|
+
case "server.instance.disposed":
|
|
14489
|
+
return "sessionEnd";
|
|
14490
|
+
default:
|
|
14491
|
+
return "unknown";
|
|
14492
|
+
}
|
|
14493
|
+
}
|
|
13871
14494
|
function parseToolArg2(argv) {
|
|
13872
14495
|
for (let i = 0; i < argv.length; i++) {
|
|
13873
14496
|
const a = argv[i];
|
|
@@ -13887,25 +14510,91 @@ async function readStdin2() {
|
|
|
13887
14510
|
function resolveCwd2(payload) {
|
|
13888
14511
|
return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
|
|
13889
14512
|
}
|
|
14513
|
+
async function repairHookForTool(repoRoot, tool) {
|
|
14514
|
+
const result = await healHookForTool(repoRoot, tool);
|
|
14515
|
+
if (result.skipped) {
|
|
14516
|
+
appendLog("warn", `self-heal: skipped ${tool} hook (${result.skipped})`);
|
|
14517
|
+
return;
|
|
14518
|
+
}
|
|
14519
|
+
if (result.changed) {
|
|
14520
|
+
appendLog("info", `self-heal: updated ${tool} hook`);
|
|
14521
|
+
}
|
|
14522
|
+
}
|
|
13890
14523
|
async function selfHealHook2(payload, tool) {
|
|
14524
|
+
if (process.env.HILLCLIMB_SKIP_HOOK_SELF_HEAL === "1") return;
|
|
13891
14525
|
const cwd = resolveCwd2(payload);
|
|
13892
14526
|
if (!cwd) return;
|
|
13893
14527
|
try {
|
|
13894
14528
|
const project = await findProjectForCwd(cwd);
|
|
13895
14529
|
if (!project) return;
|
|
13896
|
-
|
|
13897
|
-
|
|
13898
|
-
|
|
13899
|
-
|
|
14530
|
+
await repairHookForTool(project.repoRoot, tool);
|
|
14531
|
+
} catch (err) {
|
|
14532
|
+
appendLog(
|
|
14533
|
+
"warn",
|
|
14534
|
+
`self-heal: failed to repair ${tool} hook: ${formatError(err)}`
|
|
14535
|
+
);
|
|
14536
|
+
}
|
|
14537
|
+
}
|
|
14538
|
+
async function resolveLegacyBareTool(raw) {
|
|
14539
|
+
let payload;
|
|
14540
|
+
try {
|
|
14541
|
+
payload = JSON.parse(raw);
|
|
14542
|
+
} catch (err) {
|
|
14543
|
+
appendLog(
|
|
14544
|
+
"error",
|
|
14545
|
+
`git-traces: missing --tool and failed to parse payload for legacy hook repair: ${formatError(err)}`
|
|
14546
|
+
);
|
|
14547
|
+
return null;
|
|
14548
|
+
}
|
|
14549
|
+
const cwd = resolveCwd2(payload);
|
|
14550
|
+
if (!cwd) {
|
|
14551
|
+
appendLog(
|
|
14552
|
+
"error",
|
|
14553
|
+
"git-traces: missing --tool and no cwd in payload for legacy hook repair"
|
|
14554
|
+
);
|
|
14555
|
+
return null;
|
|
14556
|
+
}
|
|
14557
|
+
try {
|
|
14558
|
+
const project = await findProjectForCwd(cwd);
|
|
14559
|
+
if (!project) {
|
|
14560
|
+
appendLog(
|
|
14561
|
+
"error",
|
|
14562
|
+
`git-traces: missing --tool and no hillclimb config for cwd ${cwd}`
|
|
14563
|
+
);
|
|
14564
|
+
return null;
|
|
13900
14565
|
}
|
|
13901
|
-
|
|
13902
|
-
|
|
14566
|
+
const owners = await findLegacyGitTracesHookOwners(
|
|
14567
|
+
project.repoRoot,
|
|
14568
|
+
payload.hook_event_name
|
|
14569
|
+
);
|
|
14570
|
+
if (owners.length === 0) {
|
|
14571
|
+
appendLog(
|
|
14572
|
+
"error",
|
|
14573
|
+
`git-traces: missing --tool and no legacy bare git-traces hook matched event ${payload.hook_event_name ?? "<none>"}`
|
|
14574
|
+
);
|
|
14575
|
+
return null;
|
|
14576
|
+
}
|
|
14577
|
+
for (const owner of owners) {
|
|
14578
|
+
await repairHookForTool(project.repoRoot, owner);
|
|
13903
14579
|
}
|
|
14580
|
+
if (owners.length > 1) {
|
|
14581
|
+
appendLog(
|
|
14582
|
+
"warn",
|
|
14583
|
+
`git-traces: legacy bare hook matched multiple tools (${owners.join(", ")}); repaired hooks but skipped this event`
|
|
14584
|
+
);
|
|
14585
|
+
return null;
|
|
14586
|
+
}
|
|
14587
|
+
appendLog(
|
|
14588
|
+
"info",
|
|
14589
|
+
`git-traces: inferred legacy bare hook owner ${owners[0]} and repaired hook`
|
|
14590
|
+
);
|
|
14591
|
+
return owners[0];
|
|
13904
14592
|
} catch (err) {
|
|
13905
14593
|
appendLog(
|
|
13906
14594
|
"warn",
|
|
13907
|
-
`
|
|
14595
|
+
`git-traces: failed legacy hook repair: ${formatError(err)}`
|
|
13908
14596
|
);
|
|
14597
|
+
return null;
|
|
13909
14598
|
}
|
|
13910
14599
|
}
|
|
13911
14600
|
async function runGitTraces() {
|
|
@@ -13920,13 +14609,6 @@ async function runGitTraces() {
|
|
|
13920
14609
|
"info",
|
|
13921
14610
|
`git-traces hook invoked (pid ${process.pid}, tool=${toolArg ?? "<none>"})`
|
|
13922
14611
|
);
|
|
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
14612
|
let raw;
|
|
13931
14613
|
try {
|
|
13932
14614
|
raw = await readStdin2();
|
|
@@ -13938,6 +14620,15 @@ async function runGitTraces() {
|
|
|
13938
14620
|
appendLog("warn", "git-traces: empty stdin, expected hook payload");
|
|
13939
14621
|
return;
|
|
13940
14622
|
}
|
|
14623
|
+
let tool = toolArg;
|
|
14624
|
+
if (tool && !KNOWN_TOOLS.has(tool)) {
|
|
14625
|
+
appendLog("error", `git-traces: unknown --tool arg (got ${tool})`);
|
|
14626
|
+
return;
|
|
14627
|
+
}
|
|
14628
|
+
if (!tool) {
|
|
14629
|
+
tool = await resolveLegacyBareTool(raw);
|
|
14630
|
+
if (!tool) return;
|
|
14631
|
+
}
|
|
13941
14632
|
const entrypoint = process.argv[1];
|
|
13942
14633
|
if (!entrypoint) {
|
|
13943
14634
|
appendLog("error", "git-traces: process.argv[1] is empty");
|
|
@@ -13946,14 +14637,14 @@ async function runGitTraces() {
|
|
|
13946
14637
|
try {
|
|
13947
14638
|
const child = spawn3(
|
|
13948
14639
|
process.execPath,
|
|
13949
|
-
[entrypoint, "git-traces", `--tool=${
|
|
14640
|
+
[entrypoint, "git-traces", `--tool=${tool}`],
|
|
13950
14641
|
{
|
|
13951
14642
|
detached: true,
|
|
13952
14643
|
stdio: ["pipe", "ignore", "ignore"],
|
|
13953
14644
|
env: {
|
|
13954
14645
|
...process.env,
|
|
13955
14646
|
[WORKER_ENV_FLAG2]: "1",
|
|
13956
|
-
[TOOL_ENV_FLAG2]:
|
|
14647
|
+
[TOOL_ENV_FLAG2]: tool,
|
|
13957
14648
|
[FLOW_ID_ENV]: flowId
|
|
13958
14649
|
}
|
|
13959
14650
|
}
|
|
@@ -14024,25 +14715,14 @@ async function runGitTracesWorker() {
|
|
|
14024
14715
|
}
|
|
14025
14716
|
await selfHealHook2(payload, tool);
|
|
14026
14717
|
try {
|
|
14027
|
-
switch (event) {
|
|
14028
|
-
case "SessionStart":
|
|
14718
|
+
switch (classifyHookEvent(event)) {
|
|
14029
14719
|
case "sessionStart":
|
|
14030
|
-
// opencode: session.created fires when a new session is first opened.
|
|
14031
|
-
case "session.created":
|
|
14032
14720
|
await handleSessionStart(payload, tool);
|
|
14033
14721
|
break;
|
|
14034
|
-
case "Stop":
|
|
14035
14722
|
case "stop":
|
|
14036
|
-
// opencode: session.idle fires at end-of-turn (agent finished responding).
|
|
14037
|
-
case "session.idle":
|
|
14038
14723
|
await handleStop(payload, tool);
|
|
14039
14724
|
break;
|
|
14040
|
-
case "SessionEnd":
|
|
14041
14725
|
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
14726
|
await handleSessionEnd(payload, tool);
|
|
14047
14727
|
break;
|
|
14048
14728
|
default:
|
|
@@ -14061,14 +14741,14 @@ ${stack}` : ""}`
|
|
|
14061
14741
|
|
|
14062
14742
|
// src/outputs/zip.ts
|
|
14063
14743
|
import fs13 from "fs";
|
|
14064
|
-
import
|
|
14744
|
+
import path17 from "path";
|
|
14065
14745
|
import archiver2 from "archiver";
|
|
14066
14746
|
|
|
14067
14747
|
// src/outputs/downloads.ts
|
|
14068
14748
|
import { execSync as execSync2 } from "child_process";
|
|
14069
14749
|
import fs12 from "fs";
|
|
14070
14750
|
import os8 from "os";
|
|
14071
|
-
import
|
|
14751
|
+
import path16 from "path";
|
|
14072
14752
|
function getDownloadsFolder() {
|
|
14073
14753
|
const home = os8.homedir();
|
|
14074
14754
|
if (process.platform === "linux") {
|
|
@@ -14081,7 +14761,7 @@ function getDownloadsFolder() {
|
|
|
14081
14761
|
} catch {
|
|
14082
14762
|
}
|
|
14083
14763
|
}
|
|
14084
|
-
const downloads =
|
|
14764
|
+
const downloads = path16.join(home, "Downloads");
|
|
14085
14765
|
if (fs12.existsSync(downloads)) return downloads;
|
|
14086
14766
|
return home;
|
|
14087
14767
|
}
|
|
@@ -14091,11 +14771,11 @@ function sanitizeFilename(name) {
|
|
|
14091
14771
|
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
14092
14772
|
}
|
|
14093
14773
|
function getUniqueFilename(dir, base, ext) {
|
|
14094
|
-
let candidate =
|
|
14774
|
+
let candidate = path17.join(dir, `${base}${ext}`);
|
|
14095
14775
|
if (!fs13.existsSync(candidate)) return candidate;
|
|
14096
14776
|
let i = 1;
|
|
14097
14777
|
while (fs13.existsSync(candidate)) {
|
|
14098
|
-
candidate =
|
|
14778
|
+
candidate = path17.join(dir, `${base}-${i}${ext}`);
|
|
14099
14779
|
i++;
|
|
14100
14780
|
}
|
|
14101
14781
|
return candidate;
|
|
@@ -14105,7 +14785,7 @@ var ZipOutput = class {
|
|
|
14105
14785
|
label = "Save as .zip to Downloads";
|
|
14106
14786
|
async emit(group, options) {
|
|
14107
14787
|
const downloadsDir = getDownloadsFolder();
|
|
14108
|
-
const repoName = sanitizeFilename(
|
|
14788
|
+
const repoName = sanitizeFilename(path17.basename(group.repoPath));
|
|
14109
14789
|
const timeRange = options.timeRange;
|
|
14110
14790
|
const rangePart = timeRange?.label ?? "all";
|
|
14111
14791
|
const epochSeconds = Math.floor(Date.now() / 1e3);
|
|
@@ -14307,11 +14987,11 @@ async function confirmExport(group, output) {
|
|
|
14307
14987
|
// src/sources/claude.ts
|
|
14308
14988
|
import fs14 from "fs";
|
|
14309
14989
|
import os9 from "os";
|
|
14310
|
-
import
|
|
14990
|
+
import path18 from "path";
|
|
14311
14991
|
import readline from "readline";
|
|
14312
14992
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
|
|
14313
14993
|
async function resolveRepoPath(projectDir) {
|
|
14314
|
-
const indexPath =
|
|
14994
|
+
const indexPath = path18.join(projectDir, "sessions-index.json");
|
|
14315
14995
|
try {
|
|
14316
14996
|
const raw = await fs14.promises.readFile(indexPath, "utf-8");
|
|
14317
14997
|
const data = JSON.parse(raw);
|
|
@@ -14326,7 +15006,7 @@ async function resolveRepoPath(projectDir) {
|
|
|
14326
15006
|
});
|
|
14327
15007
|
for (const entry of entries) {
|
|
14328
15008
|
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
14329
|
-
const cwd = await extractCwdFromJsonl(
|
|
15009
|
+
const cwd = await extractCwdFromJsonl(path18.join(projectDir, entry.name));
|
|
14330
15010
|
if (cwd) {
|
|
14331
15011
|
cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
|
|
14332
15012
|
}
|
|
@@ -14372,7 +15052,7 @@ async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
|
|
|
14372
15052
|
return;
|
|
14373
15053
|
}
|
|
14374
15054
|
for (const entry of entries) {
|
|
14375
|
-
const fullPath =
|
|
15055
|
+
const fullPath = path18.join(dir, entry.name);
|
|
14376
15056
|
if (entry.isDirectory()) {
|
|
14377
15057
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
14378
15058
|
await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
|
|
@@ -14394,7 +15074,7 @@ function fallbackDecode(encodedName) {
|
|
|
14394
15074
|
var ClaudeSource = class {
|
|
14395
15075
|
name = "claude";
|
|
14396
15076
|
async scan() {
|
|
14397
|
-
const baseDir =
|
|
15077
|
+
const baseDir = path18.join(os9.homedir(), ".claude", "projects");
|
|
14398
15078
|
try {
|
|
14399
15079
|
await fs14.promises.access(baseDir);
|
|
14400
15080
|
} catch {
|
|
@@ -14406,7 +15086,7 @@ var ClaudeSource = class {
|
|
|
14406
15086
|
const dirEntries = projectDirs.filter((d) => d.isDirectory());
|
|
14407
15087
|
const resultArrays = await Promise.all(
|
|
14408
15088
|
dirEntries.map(async (dir) => {
|
|
14409
|
-
const projectPath =
|
|
15089
|
+
const projectPath = path18.join(baseDir, dir.name);
|
|
14410
15090
|
const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
|
|
14411
15091
|
const files = [];
|
|
14412
15092
|
await collectFiles(
|
|
@@ -14426,7 +15106,7 @@ var ClaudeSource = class {
|
|
|
14426
15106
|
// src/sources/codex.ts
|
|
14427
15107
|
import fs15 from "fs";
|
|
14428
15108
|
import os10 from "os";
|
|
14429
|
-
import
|
|
15109
|
+
import path19 from "path";
|
|
14430
15110
|
import readline2 from "readline";
|
|
14431
15111
|
async function parseSessionMeta(filePath) {
|
|
14432
15112
|
const stream = fs15.createReadStream(filePath, { encoding: "utf-8" });
|
|
@@ -14459,7 +15139,7 @@ async function findJsonlFiles(dir) {
|
|
|
14459
15139
|
return;
|
|
14460
15140
|
}
|
|
14461
15141
|
for (const entry of entries) {
|
|
14462
|
-
const full =
|
|
15142
|
+
const full = path19.join(d, entry.name);
|
|
14463
15143
|
if (entry.isDirectory()) {
|
|
14464
15144
|
await walk(full);
|
|
14465
15145
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -14504,14 +15184,14 @@ async function loadHistory(historyPath) {
|
|
|
14504
15184
|
var CodexSource = class {
|
|
14505
15185
|
name = "codex";
|
|
14506
15186
|
async scan() {
|
|
14507
|
-
const codexDir =
|
|
14508
|
-
const sessionsDir =
|
|
15187
|
+
const codexDir = path19.join(os10.homedir(), ".codex");
|
|
15188
|
+
const sessionsDir = path19.join(codexDir, "sessions");
|
|
14509
15189
|
try {
|
|
14510
15190
|
await fs15.promises.access(sessionsDir);
|
|
14511
15191
|
} catch {
|
|
14512
15192
|
return [];
|
|
14513
15193
|
}
|
|
14514
|
-
const historyPath =
|
|
15194
|
+
const historyPath = path19.join(codexDir, "history.jsonl");
|
|
14515
15195
|
const [jsonlFiles, historyMap] = await Promise.all([
|
|
14516
15196
|
findJsonlFiles(sessionsDir),
|
|
14517
15197
|
loadHistory(historyPath)
|
|
@@ -14534,8 +15214,8 @@ var CodexSource = class {
|
|
|
14534
15214
|
});
|
|
14535
15215
|
const historyLines = historyMap.get(meta.sessionId);
|
|
14536
15216
|
if (historyLines) {
|
|
14537
|
-
const sessionDir =
|
|
14538
|
-
const historyAbsPath =
|
|
15217
|
+
const sessionDir = path19.relative(sessionsDir, path19.dirname(filePath));
|
|
15218
|
+
const historyAbsPath = path19.join(
|
|
14539
15219
|
sessionsDir,
|
|
14540
15220
|
sessionDir,
|
|
14541
15221
|
`history-${meta.sessionId}.jsonl`
|
|
@@ -14557,16 +15237,16 @@ var CodexSource = class {
|
|
|
14557
15237
|
// src/sources/copilotChat.ts
|
|
14558
15238
|
import fs16 from "fs";
|
|
14559
15239
|
import os11 from "os";
|
|
14560
|
-
import
|
|
15240
|
+
import path20 from "path";
|
|
14561
15241
|
import { fileURLToPath } from "url";
|
|
14562
15242
|
function vsCodeUserDirs() {
|
|
14563
15243
|
const home = os11.homedir();
|
|
14564
15244
|
const dirs = [
|
|
14565
|
-
|
|
14566
|
-
|
|
15245
|
+
path20.join(home, "Library", "Application Support", "Code", "User"),
|
|
15246
|
+
path20.join(home, ".config", "Code", "User")
|
|
14567
15247
|
];
|
|
14568
15248
|
if (process.env.APPDATA) {
|
|
14569
|
-
dirs.push(
|
|
15249
|
+
dirs.push(path20.join(process.env.APPDATA, "Code", "User"));
|
|
14570
15250
|
}
|
|
14571
15251
|
return dirs;
|
|
14572
15252
|
}
|
|
@@ -14603,7 +15283,7 @@ var CopilotChatSource = class {
|
|
|
14603
15283
|
async scan() {
|
|
14604
15284
|
const results = [];
|
|
14605
15285
|
for (const userDir of vsCodeUserDirs()) {
|
|
14606
|
-
const workspaceStorage =
|
|
15286
|
+
const workspaceStorage = path20.join(userDir, "workspaceStorage");
|
|
14607
15287
|
let hashDirs;
|
|
14608
15288
|
try {
|
|
14609
15289
|
hashDirs = await fs16.promises.readdir(workspaceStorage, {
|
|
@@ -14614,8 +15294,8 @@ var CopilotChatSource = class {
|
|
|
14614
15294
|
}
|
|
14615
15295
|
for (const hash of hashDirs) {
|
|
14616
15296
|
if (!hash.isDirectory()) continue;
|
|
14617
|
-
const wsRoot =
|
|
14618
|
-
const transcriptsDir =
|
|
15297
|
+
const wsRoot = path20.join(workspaceStorage, hash.name);
|
|
15298
|
+
const transcriptsDir = path20.join(
|
|
14619
15299
|
wsRoot,
|
|
14620
15300
|
"GitHub.copilot-chat",
|
|
14621
15301
|
"transcripts"
|
|
@@ -14629,7 +15309,7 @@ var CopilotChatSource = class {
|
|
|
14629
15309
|
continue;
|
|
14630
15310
|
}
|
|
14631
15311
|
const repoPath = await readWorkspaceFolder(
|
|
14632
|
-
|
|
15312
|
+
path20.join(wsRoot, "workspace.json")
|
|
14633
15313
|
);
|
|
14634
15314
|
if (!repoPath) continue;
|
|
14635
15315
|
for (const entry of transcriptEntries) {
|
|
@@ -14637,7 +15317,7 @@ var CopilotChatSource = class {
|
|
|
14637
15317
|
const sessionId = entry.name.slice(0, -".jsonl".length);
|
|
14638
15318
|
results.push({
|
|
14639
15319
|
sourceName: this.name,
|
|
14640
|
-
absolutePath:
|
|
15320
|
+
absolutePath: path20.join(transcriptsDir, entry.name),
|
|
14641
15321
|
repoPath,
|
|
14642
15322
|
metadata: { sessionId }
|
|
14643
15323
|
});
|
|
@@ -14704,10 +15384,10 @@ async function runInteractive() {
|
|
|
14704
15384
|
s.start(`Scanning ${source.name} logs...`);
|
|
14705
15385
|
const allFiles = await source.scan();
|
|
14706
15386
|
const allGroups = await mergeByRepo(allFiles);
|
|
14707
|
-
const repoRoot =
|
|
15387
|
+
const repoRoot = path21.resolve(repo.root);
|
|
14708
15388
|
const matching = allGroups.filter((g) => {
|
|
14709
|
-
const resolved =
|
|
14710
|
-
return resolved === repoRoot || resolved.startsWith(repoRoot +
|
|
15389
|
+
const resolved = path21.resolve(g.repoPath);
|
|
15390
|
+
return resolved === repoRoot || resolved.startsWith(repoRoot + path21.sep);
|
|
14711
15391
|
});
|
|
14712
15392
|
if (matching.length === 0) {
|
|
14713
15393
|
s.stop(`No ${source.name} logs found for ${repo.name}.`);
|
|
@@ -14738,7 +15418,7 @@ async function runInteractive() {
|
|
|
14738
15418
|
}
|
|
14739
15419
|
}
|
|
14740
15420
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
14741
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
15421
|
+
const envFilePaths = envFileNames.map((n) => path21.join(repoRoot, n));
|
|
14742
15422
|
const additionalFiles = await promptSecretFiles(envFileNames);
|
|
14743
15423
|
const secretResult = await collectSecrets(
|
|
14744
15424
|
repoRoot,
|