hillclimb 0.1.9 → 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/README.md +2 -2
- package/dist/cli.js +1930 -375
- 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
|
|
@@ -864,20 +868,54 @@ import path from "node:path";
|
|
|
864
868
|
const TOOL = "opencode";
|
|
865
869
|
const TRANSCRIPT_DIR = path.join(os.homedir(), ".hillclimb", "opencode-transcripts");
|
|
866
870
|
|
|
867
|
-
// sessionID -> Map<messageID,
|
|
868
|
-
// message.updated
|
|
869
|
-
//
|
|
871
|
+
// sessionID -> Map<messageID, { info, parts }>. Using a Map-of-Maps so late
|
|
872
|
+
// message.updated / message.part.updated events overwrite earlier versions
|
|
873
|
+
// instead of appending duplicates.
|
|
870
874
|
const sessionMessages = new Map();
|
|
871
875
|
|
|
872
|
-
function
|
|
873
|
-
if (!sessionID || !
|
|
876
|
+
function ensureRecord(sessionID, messageID, info) {
|
|
877
|
+
if (!sessionID || !messageID) return null;
|
|
874
878
|
let bucket = sessionMessages.get(sessionID);
|
|
875
879
|
if (!bucket) {
|
|
876
880
|
bucket = new Map();
|
|
877
881
|
sessionMessages.set(sessionID, bucket);
|
|
878
882
|
}
|
|
879
|
-
|
|
880
|
-
|
|
883
|
+
let record = bucket.get(messageID);
|
|
884
|
+
if (!record) {
|
|
885
|
+
record = {
|
|
886
|
+
info: info || { id: messageID, sessionID },
|
|
887
|
+
parts: new Map(),
|
|
888
|
+
};
|
|
889
|
+
bucket.set(messageID, record);
|
|
890
|
+
} else if (info) {
|
|
891
|
+
record.info = Object.assign({}, record.info || {}, info);
|
|
892
|
+
}
|
|
893
|
+
return record;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function ingestMessage(sessionID, info) {
|
|
897
|
+
if (!sessionID || !info) return;
|
|
898
|
+
const id = info.id || ((info.role || "unknown") + "-" + Date.now());
|
|
899
|
+
ensureRecord(sessionID, id, info);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function ingestPart(sessionID, part) {
|
|
903
|
+
if (!part) return;
|
|
904
|
+
const sid = sessionID || part.sessionID;
|
|
905
|
+
const messageID = part.messageID;
|
|
906
|
+
if (!sid || !messageID) return;
|
|
907
|
+
const record = ensureRecord(sid, messageID, { id: messageID, sessionID: sid });
|
|
908
|
+
if (!record) return;
|
|
909
|
+
const partID = part.id || ((part.type || "part") + "-" + record.parts.size);
|
|
910
|
+
record.parts.set(partID, part);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function serializeRecord(record) {
|
|
914
|
+
const info = record.info || {};
|
|
915
|
+
const parts = Array.from(record.parts.values());
|
|
916
|
+
if (parts.length === 0) return info;
|
|
917
|
+
// Preserve the old top-level message-info shape while adding parts.
|
|
918
|
+
return Object.assign({}, info, { parts });
|
|
881
919
|
}
|
|
882
920
|
|
|
883
921
|
function writeTranscript(sessionID) {
|
|
@@ -887,7 +925,7 @@ function writeTranscript(sessionID) {
|
|
|
887
925
|
fs.mkdirSync(TRANSCRIPT_DIR, { recursive: true });
|
|
888
926
|
const file = path.join(TRANSCRIPT_DIR, sessionID + ".jsonl");
|
|
889
927
|
const lines = [];
|
|
890
|
-
for (const
|
|
928
|
+
for (const record of bucket.values()) lines.push(JSON.stringify(serializeRecord(record)));
|
|
891
929
|
fs.writeFileSync(file, lines.join("\\n") + "\\n");
|
|
892
930
|
return file;
|
|
893
931
|
} catch {
|
|
@@ -917,17 +955,27 @@ export const HillclimbPlugin = async ({ directory }) => ({
|
|
|
917
955
|
event: async ({ event }) => {
|
|
918
956
|
const type = event && event.type;
|
|
919
957
|
const props = (event && event.properties) || {};
|
|
920
|
-
const
|
|
958
|
+
const info = props.info || {};
|
|
959
|
+
const sessionID =
|
|
960
|
+
props.sessionID ||
|
|
961
|
+
info.sessionID ||
|
|
962
|
+
(type === "message.updated" || type === "message.part.updated" ? undefined : info.id);
|
|
963
|
+
const cwd = directory || props.directory || info.directory;
|
|
921
964
|
|
|
922
965
|
if (type === "message.updated") {
|
|
923
|
-
ingestMessage(sessionID,
|
|
966
|
+
ingestMessage(sessionID, info);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (type === "message.part.updated") {
|
|
971
|
+
ingestPart(sessionID, props.part);
|
|
924
972
|
return;
|
|
925
973
|
}
|
|
926
974
|
|
|
927
975
|
if (type === "session.created" && sessionID) {
|
|
928
976
|
spawnHillclimb("git-traces --tool=opencode", {
|
|
929
977
|
session_id: sessionID,
|
|
930
|
-
cwd
|
|
978
|
+
cwd,
|
|
931
979
|
hook_event_name: "session.created",
|
|
932
980
|
tool: TOOL,
|
|
933
981
|
});
|
|
@@ -940,7 +988,7 @@ export const HillclimbPlugin = async ({ directory }) => ({
|
|
|
940
988
|
// so a single session produces one contribution, not one per turn.
|
|
941
989
|
spawnHillclimb("git-traces --tool=opencode", {
|
|
942
990
|
session_id: sessionID,
|
|
943
|
-
cwd
|
|
991
|
+
cwd,
|
|
944
992
|
hook_event_name: "session.idle",
|
|
945
993
|
tool: TOOL,
|
|
946
994
|
});
|
|
@@ -949,45 +997,53 @@ export const HillclimbPlugin = async ({ directory }) => ({
|
|
|
949
997
|
|
|
950
998
|
if (type === "session.deleted" && sessionID) {
|
|
951
999
|
// User explicitly ended this session. Upload its transcript and drop
|
|
952
|
-
// the buffer.
|
|
953
|
-
//
|
|
954
|
-
// keyed per (repoRoot, tool) \u2014 clearing it here would wipe their
|
|
955
|
-
// concurrent session. Cleanup happens at server.instance.disposed
|
|
956
|
-
// (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.
|
|
957
1002
|
const transcriptPath = writeTranscript(sessionID);
|
|
958
1003
|
if (transcriptPath) {
|
|
959
1004
|
spawnHillclimb("upload", {
|
|
960
1005
|
session_id: sessionID,
|
|
961
|
-
cwd
|
|
1006
|
+
cwd,
|
|
962
1007
|
transcript_path: transcriptPath,
|
|
963
1008
|
hook_event_name: "session.deleted",
|
|
964
1009
|
tool: TOOL,
|
|
965
1010
|
});
|
|
966
1011
|
}
|
|
1012
|
+
spawnHillclimb("git-traces --tool=opencode", {
|
|
1013
|
+
session_id: sessionID,
|
|
1014
|
+
cwd,
|
|
1015
|
+
hook_event_name: "session.deleted",
|
|
1016
|
+
tool: TOOL,
|
|
1017
|
+
});
|
|
967
1018
|
sessionMessages.delete(sessionID);
|
|
968
1019
|
return;
|
|
969
1020
|
}
|
|
970
1021
|
|
|
971
1022
|
if (type === "server.instance.disposed") {
|
|
972
1023
|
// opencode is shutting down \u2014 flush every buffered session's transcript
|
|
973
|
-
// so nothing gets lost. Then clean up git-traces refs/state.
|
|
974
|
-
// sessionID on this event, but git-traces state is keyed by
|
|
975
|
-
// (repoRoot, tool) so cwd alone is enough for handleSessionEnd.
|
|
1024
|
+
// so nothing gets lost. Then clean up git-traces refs/state per session.
|
|
976
1025
|
for (const sid of sessionMessages.keys()) {
|
|
977
1026
|
const transcriptPath = writeTranscript(sid);
|
|
978
1027
|
if (transcriptPath) {
|
|
979
1028
|
spawnHillclimb("upload", {
|
|
980
1029
|
session_id: sid,
|
|
981
|
-
cwd
|
|
1030
|
+
cwd,
|
|
982
1031
|
transcript_path: transcriptPath,
|
|
983
1032
|
hook_event_name: "server.instance.disposed",
|
|
984
1033
|
tool: TOOL,
|
|
985
1034
|
});
|
|
986
1035
|
}
|
|
1036
|
+
spawnHillclimb("git-traces --tool=opencode", {
|
|
1037
|
+
session_id: sid,
|
|
1038
|
+
cwd,
|
|
1039
|
+
hook_event_name: "server.instance.disposed",
|
|
1040
|
+
tool: TOOL,
|
|
1041
|
+
});
|
|
987
1042
|
}
|
|
988
1043
|
sessionMessages.clear();
|
|
1044
|
+
// Keep a no-session cleanup for pre-session-scoped legacy state.
|
|
989
1045
|
spawnHillclimb("git-traces --tool=opencode", {
|
|
990
|
-
cwd
|
|
1046
|
+
cwd,
|
|
991
1047
|
hook_event_name: "server.instance.disposed",
|
|
992
1048
|
tool: TOOL,
|
|
993
1049
|
});
|
|
@@ -998,16 +1054,16 @@ export const HillclimbPlugin = async ({ directory }) => ({
|
|
|
998
1054
|
|
|
999
1055
|
export default HillclimbPlugin;
|
|
1000
1056
|
`;
|
|
1001
|
-
async function opencodeInstall(file) {
|
|
1057
|
+
async function opencodeInstall(file, content = OPENCODE_PLUGIN_CONTENT) {
|
|
1002
1058
|
try {
|
|
1003
1059
|
const existing = await fs5.promises.readFile(file, "utf-8");
|
|
1004
|
-
if (existing ===
|
|
1060
|
+
if (existing === content) {
|
|
1005
1061
|
return { installed: 0, alreadyPresent: 1 };
|
|
1006
1062
|
}
|
|
1007
1063
|
} catch {
|
|
1008
1064
|
}
|
|
1009
1065
|
await fs5.promises.mkdir(path6.dirname(file), { recursive: true });
|
|
1010
|
-
await fs5.promises.writeFile(file,
|
|
1066
|
+
await fs5.promises.writeFile(file, content);
|
|
1011
1067
|
return { installed: 1, alreadyPresent: 0 };
|
|
1012
1068
|
}
|
|
1013
1069
|
async function opencodeCheck(file) {
|
|
@@ -1038,14 +1094,24 @@ function check(settings, format, eventName, command, options = {}) {
|
|
|
1038
1094
|
return claudeCheck(settings, eventName, command, options);
|
|
1039
1095
|
}
|
|
1040
1096
|
}
|
|
1041
|
-
function
|
|
1097
|
+
function commandsForEvent(settings, format, eventName) {
|
|
1098
|
+
switch (format) {
|
|
1099
|
+
case "cursor":
|
|
1100
|
+
return (settings.hooks?.[eventName] ?? []).map((e) => e.command).filter((command) => typeof command === "string");
|
|
1101
|
+
case "copilot":
|
|
1102
|
+
return (settings.hooks?.[eventName] ?? []).filter((e) => e.type === "command").map((e) => e.command).filter((command) => typeof command === "string");
|
|
1103
|
+
default:
|
|
1104
|
+
return (settings.hooks?.[eventName] ?? []).flatMap((matcher) => matcher.hooks ?? []).filter((hook) => hook.type === "command").map((hook) => hook.command).filter((command) => typeof command === "string");
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function uninstallMatching(settings, format, eventName, predicate) {
|
|
1042
1108
|
switch (format) {
|
|
1043
1109
|
case "cursor":
|
|
1044
|
-
return
|
|
1110
|
+
return cursorUninstallMatching(settings, eventName, predicate);
|
|
1045
1111
|
case "copilot":
|
|
1046
|
-
return
|
|
1112
|
+
return copilotUninstallMatching(settings, eventName, predicate);
|
|
1047
1113
|
default:
|
|
1048
|
-
return
|
|
1114
|
+
return claudeUninstallMatching(settings, eventName, predicate);
|
|
1049
1115
|
}
|
|
1050
1116
|
}
|
|
1051
1117
|
function legacyCommandsFor(command) {
|
|
@@ -1066,21 +1132,38 @@ function legacyCommandsFor(command) {
|
|
|
1066
1132
|
}
|
|
1067
1133
|
return legacy;
|
|
1068
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
|
+
}
|
|
1069
1149
|
async function installHooksForTool(repoRoot, def) {
|
|
1070
1150
|
const file = settingsPath(repoRoot, def);
|
|
1071
1151
|
if (def.format === "opencode") {
|
|
1072
1152
|
const r = await opencodeInstall(file);
|
|
1073
|
-
return { settingsFile: file, ...r };
|
|
1153
|
+
return { settingsFile: file, ...r, changed: r.installed > 0 };
|
|
1074
1154
|
}
|
|
1075
1155
|
const settings = await readJson(file);
|
|
1076
1156
|
let installed = 0;
|
|
1077
1157
|
let alreadyPresent = 0;
|
|
1078
1158
|
let mutated = false;
|
|
1079
1159
|
for (const evt of def.events) {
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1160
|
+
if (uninstallMatching(
|
|
1161
|
+
settings,
|
|
1162
|
+
def.format,
|
|
1163
|
+
evt.eventName,
|
|
1164
|
+
(command) => command !== evt.command && isHillclimbOwnedCommandFor(command, evt.command)
|
|
1165
|
+
)) {
|
|
1166
|
+
mutated = true;
|
|
1084
1167
|
}
|
|
1085
1168
|
const added = install(settings, def.format, evt.eventName, evt.command, {
|
|
1086
1169
|
timeout: evt.timeout
|
|
@@ -1093,7 +1176,7 @@ async function installHooksForTool(repoRoot, def) {
|
|
|
1093
1176
|
}
|
|
1094
1177
|
}
|
|
1095
1178
|
if (mutated) await writeJson(file, settings);
|
|
1096
|
-
return { settingsFile: file, installed, alreadyPresent };
|
|
1179
|
+
return { settingsFile: file, installed, alreadyPresent, changed: mutated };
|
|
1097
1180
|
}
|
|
1098
1181
|
async function checkHooksForTool(repoRoot, def) {
|
|
1099
1182
|
const file = settingsPath(repoRoot, def);
|
|
@@ -1116,6 +1199,27 @@ async function checkHooksForTool(repoRoot, def) {
|
|
|
1116
1199
|
}
|
|
1117
1200
|
return { allInstalled: missing.length === 0, settingsFile: file, missing };
|
|
1118
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
|
+
}
|
|
1119
1223
|
function detectTools(repoRoot) {
|
|
1120
1224
|
return TOOLS.filter((def) => def.detect(repoRoot));
|
|
1121
1225
|
}
|
|
@@ -1133,6 +1237,29 @@ async function installDetectedHooks(repoRoot) {
|
|
|
1133
1237
|
}
|
|
1134
1238
|
return results;
|
|
1135
1239
|
}
|
|
1240
|
+
async function healHookForTool(repoRoot, tool) {
|
|
1241
|
+
const def = TOOLS.find((candidate) => candidate.tool === tool);
|
|
1242
|
+
if (!def) {
|
|
1243
|
+
return {
|
|
1244
|
+
tool,
|
|
1245
|
+
changed: false,
|
|
1246
|
+
alreadyCurrent: false,
|
|
1247
|
+
installed: 0,
|
|
1248
|
+
alreadyPresent: 0,
|
|
1249
|
+
skipped: "unsupported-tool"
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
const r = await installHooksForTool(repoRoot, def);
|
|
1253
|
+
return {
|
|
1254
|
+
tool: def.tool,
|
|
1255
|
+
label: def.label,
|
|
1256
|
+
settingsFile: r.settingsFile,
|
|
1257
|
+
changed: r.changed,
|
|
1258
|
+
alreadyCurrent: !r.changed,
|
|
1259
|
+
installed: r.installed,
|
|
1260
|
+
alreadyPresent: r.alreadyPresent
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1136
1263
|
async function checkAllHooks(repoRoot) {
|
|
1137
1264
|
const results = [];
|
|
1138
1265
|
for (const def of TOOLS) {
|
|
@@ -11240,6 +11367,59 @@ function parseOutputBlob(raw) {
|
|
|
11240
11367
|
}
|
|
11241
11368
|
return [String(parsed), void 0];
|
|
11242
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
|
+
}
|
|
11243
11423
|
function convertCodexToTrajectory(jsonlContent, sessionId) {
|
|
11244
11424
|
const rawEvents = [];
|
|
11245
11425
|
for (const line of jsonlContent.split("\n")) {
|
|
@@ -11253,7 +11433,7 @@ function convertCodexToTrajectory(jsonlContent, sessionId) {
|
|
|
11253
11433
|
if (rawEvents.length === 0) return null;
|
|
11254
11434
|
const sessionMeta = rawEvents.find((e) => e.type === "session_meta");
|
|
11255
11435
|
const metaPayload = sessionMeta?.payload ?? {};
|
|
11256
|
-
const sid = sessionId ?? metaPayload.id
|
|
11436
|
+
const sid = sessionId ?? (typeof metaPayload.id === "string" ? metaPayload.id : "");
|
|
11257
11437
|
const agentVersion = metaPayload.cli_version ?? "unknown";
|
|
11258
11438
|
const agentExtra = {};
|
|
11259
11439
|
for (const key of ["originator", "cwd", "git", "instructions"]) {
|
|
@@ -11370,6 +11550,13 @@ function convertCodexToTrajectory(jsonlContent, sessionId) {
|
|
|
11370
11550
|
callInfo.output = outputText;
|
|
11371
11551
|
callInfo.metadata = metadata;
|
|
11372
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
|
+
}
|
|
11373
11560
|
normalizedEvents.push(callInfo);
|
|
11374
11561
|
pendingReasoning = void 0;
|
|
11375
11562
|
}
|
|
@@ -11425,7 +11612,8 @@ function convertCodexToTrajectory(jsonlContent, sessionId) {
|
|
|
11425
11612
|
extra: Object.keys(agentExtra).length > 0 ? agentExtra : void 0
|
|
11426
11613
|
},
|
|
11427
11614
|
steps,
|
|
11428
|
-
final_metrics: finalMetrics
|
|
11615
|
+
final_metrics: finalMetrics,
|
|
11616
|
+
extra: codexTrajectoryExtra(metaPayload)
|
|
11429
11617
|
};
|
|
11430
11618
|
}
|
|
11431
11619
|
function convertEventToStep2(event, stepId, defaultModelName) {
|
|
@@ -11460,7 +11648,8 @@ function convertEventToStep2(event, stepId, defaultModelName) {
|
|
|
11460
11648
|
if (event.output !== void 0) {
|
|
11461
11649
|
const result = {
|
|
11462
11650
|
source_call_id: callId || void 0,
|
|
11463
|
-
content: event.output
|
|
11651
|
+
content: event.output,
|
|
11652
|
+
subagent_trajectory_ref: event.subagentRefs
|
|
11464
11653
|
};
|
|
11465
11654
|
observation = { results: [result] };
|
|
11466
11655
|
}
|
|
@@ -11488,6 +11677,288 @@ function convertEventToStep2(event, stepId, defaultModelName) {
|
|
|
11488
11677
|
return null;
|
|
11489
11678
|
}
|
|
11490
11679
|
|
|
11680
|
+
// src/normalizer/types.ts
|
|
11681
|
+
var ATIF_VERSION = "ATIF-v1.6";
|
|
11682
|
+
function excludeNone(obj) {
|
|
11683
|
+
const result = {};
|
|
11684
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
11685
|
+
if (value === void 0 || value === null) continue;
|
|
11686
|
+
if (Array.isArray(value)) {
|
|
11687
|
+
result[key] = value.map(
|
|
11688
|
+
(item) => typeof item === "object" && item !== null && !Array.isArray(item) ? excludeNone(item) : item
|
|
11689
|
+
);
|
|
11690
|
+
} else if (typeof value === "object" && !Array.isArray(value)) {
|
|
11691
|
+
result[key] = excludeNone(value);
|
|
11692
|
+
} else {
|
|
11693
|
+
result[key] = value;
|
|
11694
|
+
}
|
|
11695
|
+
}
|
|
11696
|
+
return result;
|
|
11697
|
+
}
|
|
11698
|
+
|
|
11699
|
+
// src/normalizer/copilotChat.ts
|
|
11700
|
+
function asObject2(value) {
|
|
11701
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
11702
|
+
}
|
|
11703
|
+
function asNumber(value) {
|
|
11704
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
11705
|
+
}
|
|
11706
|
+
function compactExtra2(extra) {
|
|
11707
|
+
const result = {};
|
|
11708
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
11709
|
+
if (value !== void 0 && value !== null) result[key] = value;
|
|
11710
|
+
}
|
|
11711
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
11712
|
+
}
|
|
11713
|
+
function parseJsonLines(content) {
|
|
11714
|
+
const entries = [];
|
|
11715
|
+
for (const line of content.split("\n")) {
|
|
11716
|
+
const trimmed = line.trim();
|
|
11717
|
+
if (!trimmed) continue;
|
|
11718
|
+
try {
|
|
11719
|
+
const parsed = asObject2(JSON.parse(trimmed));
|
|
11720
|
+
if (!parsed) continue;
|
|
11721
|
+
entries.push({
|
|
11722
|
+
type: typeof parsed.type === "string" ? parsed.type : void 0,
|
|
11723
|
+
data: asObject2(parsed.data),
|
|
11724
|
+
id: typeof parsed.id === "string" ? parsed.id : void 0,
|
|
11725
|
+
timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : void 0
|
|
11726
|
+
});
|
|
11727
|
+
} catch {
|
|
11728
|
+
}
|
|
11729
|
+
}
|
|
11730
|
+
return entries;
|
|
11731
|
+
}
|
|
11732
|
+
function parseArguments(value) {
|
|
11733
|
+
const obj = asObject2(value);
|
|
11734
|
+
if (obj) return obj;
|
|
11735
|
+
if (typeof value === "string") {
|
|
11736
|
+
try {
|
|
11737
|
+
const parsed = asObject2(JSON.parse(value));
|
|
11738
|
+
if (parsed) return parsed;
|
|
11739
|
+
} catch {
|
|
11740
|
+
return value ? { input: value } : {};
|
|
11741
|
+
}
|
|
11742
|
+
}
|
|
11743
|
+
return value === void 0 || value === null ? {} : { value };
|
|
11744
|
+
}
|
|
11745
|
+
function buildMetrics2(data) {
|
|
11746
|
+
const inputTokens = asNumber(data.inputTokens) ?? 0;
|
|
11747
|
+
const outputTokens = asNumber(data.outputTokens) ?? 0;
|
|
11748
|
+
const cacheReadTokens = asNumber(data.cacheReadTokens) ?? 0;
|
|
11749
|
+
const cacheWriteTokens = asNumber(data.cacheWriteTokens) ?? 0;
|
|
11750
|
+
const cost = asNumber(data.cost);
|
|
11751
|
+
if (!inputTokens && !outputTokens && !cacheReadTokens && !cacheWriteTokens && !cost) {
|
|
11752
|
+
return void 0;
|
|
11753
|
+
}
|
|
11754
|
+
return {
|
|
11755
|
+
prompt_tokens: inputTokens + cacheReadTokens || void 0,
|
|
11756
|
+
completion_tokens: outputTokens || void 0,
|
|
11757
|
+
cached_tokens: cacheReadTokens || void 0,
|
|
11758
|
+
cost_usd: cost || void 0,
|
|
11759
|
+
extra: compactExtra2({
|
|
11760
|
+
cache_write_tokens: cacheWriteTokens || void 0,
|
|
11761
|
+
duration_ms: data.duration,
|
|
11762
|
+
initiator: data.initiator,
|
|
11763
|
+
api_call_id: data.apiCallId,
|
|
11764
|
+
provider_call_id: data.providerCallId,
|
|
11765
|
+
parent_tool_call_id: data.parentToolCallId,
|
|
11766
|
+
quota_snapshots: data.quotaSnapshots,
|
|
11767
|
+
copilot_usage: data.copilotUsage
|
|
11768
|
+
})
|
|
11769
|
+
};
|
|
11770
|
+
}
|
|
11771
|
+
function finalMetricsFromSteps(steps) {
|
|
11772
|
+
let prompt = 0;
|
|
11773
|
+
let completion = 0;
|
|
11774
|
+
let cached = 0;
|
|
11775
|
+
let cost = 0;
|
|
11776
|
+
for (const step of steps) {
|
|
11777
|
+
prompt += step.metrics?.prompt_tokens ?? 0;
|
|
11778
|
+
completion += step.metrics?.completion_tokens ?? 0;
|
|
11779
|
+
cached += step.metrics?.cached_tokens ?? 0;
|
|
11780
|
+
cost += step.metrics?.cost_usd ?? 0;
|
|
11781
|
+
}
|
|
11782
|
+
return {
|
|
11783
|
+
total_prompt_tokens: prompt || void 0,
|
|
11784
|
+
total_completion_tokens: completion || void 0,
|
|
11785
|
+
total_cached_tokens: cached || void 0,
|
|
11786
|
+
total_cost_usd: cost || void 0,
|
|
11787
|
+
total_steps: steps.length
|
|
11788
|
+
};
|
|
11789
|
+
}
|
|
11790
|
+
function makeToolCall(request) {
|
|
11791
|
+
const req = asObject2(request);
|
|
11792
|
+
if (!req) return null;
|
|
11793
|
+
const callId = typeof req.toolCallId === "string" && req.toolCallId || typeof req.id === "string" && req.id || "";
|
|
11794
|
+
const name = typeof req.name === "string" && req.name || typeof req.toolName === "string" && req.toolName || "tool";
|
|
11795
|
+
return {
|
|
11796
|
+
tool_call_id: callId,
|
|
11797
|
+
function_name: name,
|
|
11798
|
+
arguments: parseArguments(req.arguments)
|
|
11799
|
+
};
|
|
11800
|
+
}
|
|
11801
|
+
function toolCallFromExecutionStart(data) {
|
|
11802
|
+
const callId = typeof data.toolCallId === "string" && data.toolCallId || typeof data.id === "string" && data.id || "";
|
|
11803
|
+
const name = typeof data.toolName === "string" && data.toolName || typeof data.name === "string" && data.name || "tool";
|
|
11804
|
+
return {
|
|
11805
|
+
tool_call_id: callId,
|
|
11806
|
+
function_name: name,
|
|
11807
|
+
arguments: parseArguments(data.arguments)
|
|
11808
|
+
};
|
|
11809
|
+
}
|
|
11810
|
+
function contentFromToolResult(data) {
|
|
11811
|
+
const result = asObject2(data.result);
|
|
11812
|
+
if (typeof result?.content === "string") return result.content;
|
|
11813
|
+
if (typeof data.content === "string") return data.content;
|
|
11814
|
+
return void 0;
|
|
11815
|
+
}
|
|
11816
|
+
function convertCopilotChatToTrajectory(jsonlContent, sessionId) {
|
|
11817
|
+
const entries = parseJsonLines(jsonlContent);
|
|
11818
|
+
if (entries.length === 0) return null;
|
|
11819
|
+
let sid = sessionId ?? "unknown";
|
|
11820
|
+
let copilotVersion = "unknown";
|
|
11821
|
+
let vscodeVersion;
|
|
11822
|
+
let cwd;
|
|
11823
|
+
let defaultModelName;
|
|
11824
|
+
const steps = [];
|
|
11825
|
+
const pendingReasoning = [];
|
|
11826
|
+
const pendingToolSteps = /* @__PURE__ */ new Map();
|
|
11827
|
+
let lastAgentStep;
|
|
11828
|
+
for (const entry of entries) {
|
|
11829
|
+
const data = entry.data ?? {};
|
|
11830
|
+
if (entry.type === "session.start") {
|
|
11831
|
+
if (!sessionId && typeof data.sessionId === "string")
|
|
11832
|
+
sid = data.sessionId;
|
|
11833
|
+
if (typeof data.copilotVersion === "string")
|
|
11834
|
+
copilotVersion = data.copilotVersion;
|
|
11835
|
+
if (typeof data.vscodeVersion === "string")
|
|
11836
|
+
vscodeVersion = data.vscodeVersion;
|
|
11837
|
+
const context = asObject2(data.context);
|
|
11838
|
+
if (typeof context?.cwd === "string") cwd = context.cwd;
|
|
11839
|
+
continue;
|
|
11840
|
+
}
|
|
11841
|
+
if (entry.type === "user.message" || entry.type === "system.message") {
|
|
11842
|
+
const content = typeof data.content === "string" && data.content || typeof data.transformedContent === "string" && data.transformedContent || "";
|
|
11843
|
+
if (!content.trim()) continue;
|
|
11844
|
+
const source = entry.type === "system.message" ? "system" : "user";
|
|
11845
|
+
const step = {
|
|
11846
|
+
step_id: steps.length + 1,
|
|
11847
|
+
timestamp: entry.timestamp,
|
|
11848
|
+
source,
|
|
11849
|
+
message: content,
|
|
11850
|
+
extra: compactExtra2({
|
|
11851
|
+
attachments: data.attachments,
|
|
11852
|
+
source: data.source,
|
|
11853
|
+
agent_mode: data.agentMode,
|
|
11854
|
+
interaction_id: data.interactionId
|
|
11855
|
+
})
|
|
11856
|
+
};
|
|
11857
|
+
steps.push(step);
|
|
11858
|
+
continue;
|
|
11859
|
+
}
|
|
11860
|
+
if (entry.type === "assistant.reasoning") {
|
|
11861
|
+
if (typeof data.content === "string" && data.content.trim()) {
|
|
11862
|
+
pendingReasoning.push(data.content.trim());
|
|
11863
|
+
}
|
|
11864
|
+
continue;
|
|
11865
|
+
}
|
|
11866
|
+
if (entry.type === "assistant.message") {
|
|
11867
|
+
const toolCalls = (Array.isArray(data.toolRequests) ? data.toolRequests : []).flatMap((request) => {
|
|
11868
|
+
const call = makeToolCall(request);
|
|
11869
|
+
return call ? [call] : [];
|
|
11870
|
+
});
|
|
11871
|
+
const content = typeof data.content === "string" && data.content.trim() ? data.content.trim() : "";
|
|
11872
|
+
const reasoning = typeof data.reasoningText === "string" && data.reasoningText.trim() || pendingReasoning.join("\n\n") || void 0;
|
|
11873
|
+
pendingReasoning.length = 0;
|
|
11874
|
+
const step = {
|
|
11875
|
+
step_id: steps.length + 1,
|
|
11876
|
+
timestamp: entry.timestamp,
|
|
11877
|
+
source: "agent",
|
|
11878
|
+
message: content || (toolCalls.length > 0 ? "(tool use)" : ""),
|
|
11879
|
+
model_name: defaultModelName,
|
|
11880
|
+
extra: compactExtra2({
|
|
11881
|
+
message_id: data.messageId,
|
|
11882
|
+
phase: data.phase,
|
|
11883
|
+
output_tokens: data.outputTokens
|
|
11884
|
+
})
|
|
11885
|
+
};
|
|
11886
|
+
if (reasoning) step.reasoning_content = reasoning;
|
|
11887
|
+
if (toolCalls.length > 0) step.tool_calls = toolCalls;
|
|
11888
|
+
if (step.message || step.tool_calls?.length || step.reasoning_content) {
|
|
11889
|
+
steps.push(step);
|
|
11890
|
+
lastAgentStep = step;
|
|
11891
|
+
for (const call of toolCalls) {
|
|
11892
|
+
if (call.tool_call_id) pendingToolSteps.set(call.tool_call_id, step);
|
|
11893
|
+
}
|
|
11894
|
+
}
|
|
11895
|
+
continue;
|
|
11896
|
+
}
|
|
11897
|
+
if (entry.type === "tool.execution_start") {
|
|
11898
|
+
const toolCall = toolCallFromExecutionStart(data);
|
|
11899
|
+
const step = {
|
|
11900
|
+
step_id: steps.length + 1,
|
|
11901
|
+
timestamp: entry.timestamp,
|
|
11902
|
+
source: "agent",
|
|
11903
|
+
message: `Executed ${toolCall.function_name} ${toolCall.tool_call_id}`.trim(),
|
|
11904
|
+
model_name: defaultModelName,
|
|
11905
|
+
tool_calls: [toolCall]
|
|
11906
|
+
};
|
|
11907
|
+
steps.push(step);
|
|
11908
|
+
lastAgentStep = step;
|
|
11909
|
+
if (toolCall.tool_call_id)
|
|
11910
|
+
pendingToolSteps.set(toolCall.tool_call_id, step);
|
|
11911
|
+
continue;
|
|
11912
|
+
}
|
|
11913
|
+
if (entry.type === "tool.execution_complete") {
|
|
11914
|
+
const callId = typeof data.toolCallId === "string" && data.toolCallId || typeof data.id === "string" && data.id || "";
|
|
11915
|
+
const target = pendingToolSteps.get(callId);
|
|
11916
|
+
const content = contentFromToolResult(data);
|
|
11917
|
+
if (target && content !== void 0) {
|
|
11918
|
+
const observation = target.observation ?? { results: [] };
|
|
11919
|
+
observation.results.push({
|
|
11920
|
+
source_call_id: callId || void 0,
|
|
11921
|
+
content
|
|
11922
|
+
});
|
|
11923
|
+
target.observation = observation;
|
|
11924
|
+
const extra = { ...target.extra ?? {} };
|
|
11925
|
+
extra.tool_success = data.success;
|
|
11926
|
+
target.extra = compactExtra2(extra);
|
|
11927
|
+
}
|
|
11928
|
+
if (callId) pendingToolSteps.delete(callId);
|
|
11929
|
+
continue;
|
|
11930
|
+
}
|
|
11931
|
+
if (entry.type === "assistant.usage") {
|
|
11932
|
+
if (typeof data.model === "string" && !defaultModelName) {
|
|
11933
|
+
defaultModelName = data.model;
|
|
11934
|
+
if (lastAgentStep && !lastAgentStep.model_name) {
|
|
11935
|
+
lastAgentStep.model_name = defaultModelName;
|
|
11936
|
+
}
|
|
11937
|
+
}
|
|
11938
|
+
const metrics = buildMetrics2(data);
|
|
11939
|
+
if (metrics && lastAgentStep && !lastAgentStep.metrics) {
|
|
11940
|
+
lastAgentStep.metrics = metrics;
|
|
11941
|
+
}
|
|
11942
|
+
}
|
|
11943
|
+
}
|
|
11944
|
+
if (steps.length === 0) return null;
|
|
11945
|
+
return {
|
|
11946
|
+
schema_version: ATIF_VERSION,
|
|
11947
|
+
session_id: sid,
|
|
11948
|
+
agent: {
|
|
11949
|
+
name: "github-copilot-chat",
|
|
11950
|
+
version: copilotVersion,
|
|
11951
|
+
model_name: defaultModelName,
|
|
11952
|
+
extra: compactExtra2({
|
|
11953
|
+
vscode_version: vscodeVersion,
|
|
11954
|
+
cwd
|
|
11955
|
+
})
|
|
11956
|
+
},
|
|
11957
|
+
steps,
|
|
11958
|
+
final_metrics: finalMetricsFromSteps(steps)
|
|
11959
|
+
};
|
|
11960
|
+
}
|
|
11961
|
+
|
|
11491
11962
|
// src/normalizer/cursor.ts
|
|
11492
11963
|
function convertCursorToTrajectory(jsonlContent, sessionId) {
|
|
11493
11964
|
const lines = [];
|
|
@@ -11545,80 +12016,566 @@ function convertCursorToTrajectory(jsonlContent, sessionId) {
|
|
|
11545
12016
|
};
|
|
11546
12017
|
}
|
|
11547
12018
|
|
|
11548
|
-
// src/normalizer/
|
|
11549
|
-
function
|
|
12019
|
+
// src/normalizer/opencode.ts
|
|
12020
|
+
function asObject3(value) {
|
|
12021
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
12022
|
+
}
|
|
12023
|
+
function asArray(value) {
|
|
12024
|
+
return Array.isArray(value) ? value : [];
|
|
12025
|
+
}
|
|
12026
|
+
function asNumber2(value) {
|
|
12027
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
12028
|
+
}
|
|
12029
|
+
function compactExtra3(extra) {
|
|
11550
12030
|
const result = {};
|
|
11551
|
-
for (const [key, value] of Object.entries(
|
|
11552
|
-
if (value
|
|
11553
|
-
|
|
11554
|
-
|
|
11555
|
-
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
11559
|
-
|
|
11560
|
-
|
|
12031
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
12032
|
+
if (value !== void 0 && value !== null) result[key] = value;
|
|
12033
|
+
}
|
|
12034
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
12035
|
+
}
|
|
12036
|
+
function parseJsonLines2(content) {
|
|
12037
|
+
const events = [];
|
|
12038
|
+
for (const line of content.split("\n")) {
|
|
12039
|
+
const trimmed = line.trim();
|
|
12040
|
+
if (!trimmed) continue;
|
|
12041
|
+
try {
|
|
12042
|
+
const parsed = JSON.parse(trimmed);
|
|
12043
|
+
const obj = asObject3(parsed);
|
|
12044
|
+
if (obj) events.push(obj);
|
|
12045
|
+
} catch {
|
|
11561
12046
|
}
|
|
11562
12047
|
}
|
|
11563
|
-
return
|
|
12048
|
+
return events;
|
|
11564
12049
|
}
|
|
11565
|
-
|
|
11566
|
-
|
|
11567
|
-
|
|
11568
|
-
|
|
11569
|
-
|
|
11570
|
-
|
|
11571
|
-
|
|
11572
|
-
|
|
11573
|
-
|
|
11574
|
-
|
|
11575
|
-
|
|
11576
|
-
|
|
12050
|
+
function timestampToIso(value) {
|
|
12051
|
+
const numeric = asNumber2(value);
|
|
12052
|
+
if (numeric === void 0) return void 0;
|
|
12053
|
+
const millis = numeric < 1e12 ? numeric * 1e3 : numeric;
|
|
12054
|
+
const date = new Date(millis);
|
|
12055
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
12056
|
+
}
|
|
12057
|
+
function timeFromObject(value) {
|
|
12058
|
+
const obj = asObject3(value);
|
|
12059
|
+
if (!obj) return void 0;
|
|
12060
|
+
return timestampToIso(obj.start ?? obj.created ?? obj.completed ?? obj.end);
|
|
12061
|
+
}
|
|
12062
|
+
function stringify2(value) {
|
|
12063
|
+
if (typeof value === "string") return value;
|
|
12064
|
+
try {
|
|
12065
|
+
return JSON.stringify(value);
|
|
12066
|
+
} catch {
|
|
12067
|
+
return String(value);
|
|
11577
12068
|
}
|
|
11578
12069
|
}
|
|
11579
|
-
|
|
11580
|
-
|
|
11581
|
-
|
|
11582
|
-
|
|
11583
|
-
|
|
11584
|
-
|
|
11585
|
-
|
|
11586
|
-
if (
|
|
11587
|
-
|
|
11588
|
-
|
|
11589
|
-
|
|
11590
|
-
|
|
11591
|
-
|
|
11592
|
-
|
|
11593
|
-
|
|
11594
|
-
|
|
11595
|
-
|
|
11596
|
-
|
|
11597
|
-
|
|
11598
|
-
|
|
11599
|
-
|
|
11600
|
-
|
|
11601
|
-
|
|
11602
|
-
|
|
11603
|
-
|
|
11604
|
-
|
|
11605
|
-
|
|
11606
|
-
|
|
11607
|
-
|
|
11608
|
-
|
|
11609
|
-
|
|
11610
|
-
|
|
11611
|
-
|
|
11612
|
-
}
|
|
11613
|
-
return { ...group, files: newFiles };
|
|
12070
|
+
function argsFromUnknown(value) {
|
|
12071
|
+
const obj = asObject3(value);
|
|
12072
|
+
if (obj) return obj;
|
|
12073
|
+
if (typeof value === "string") {
|
|
12074
|
+
try {
|
|
12075
|
+
const parsed = JSON.parse(value);
|
|
12076
|
+
const parsedObj = asObject3(parsed);
|
|
12077
|
+
if (parsedObj) return parsedObj;
|
|
12078
|
+
} catch {
|
|
12079
|
+
return value ? { input: value } : {};
|
|
12080
|
+
}
|
|
12081
|
+
}
|
|
12082
|
+
return value === void 0 || value === null ? {} : { value };
|
|
12083
|
+
}
|
|
12084
|
+
function modelNameFromInfo(info) {
|
|
12085
|
+
const model = asObject3(info.model);
|
|
12086
|
+
const modelID = typeof info.modelID === "string" && info.modelID || typeof model?.modelID === "string" && model.modelID || void 0;
|
|
12087
|
+
const providerID = typeof info.providerID === "string" && info.providerID || typeof model?.providerID === "string" && model.providerID || void 0;
|
|
12088
|
+
if (providerID && modelID) return `${providerID}/${modelID}`;
|
|
12089
|
+
return modelID;
|
|
12090
|
+
}
|
|
12091
|
+
function metricsFromTokens(tokens, cost) {
|
|
12092
|
+
const t = asObject3(tokens);
|
|
12093
|
+
if (!t) return void 0;
|
|
12094
|
+
const cache = asObject3(t.cache);
|
|
12095
|
+
const input = asNumber2(t.input) ?? 0;
|
|
12096
|
+
const output = asNumber2(t.output) ?? 0;
|
|
12097
|
+
const reasoning = asNumber2(t.reasoning) ?? 0;
|
|
12098
|
+
const cacheRead = asNumber2(cache?.read) ?? 0;
|
|
12099
|
+
const cacheWrite = asNumber2(cache?.write) ?? 0;
|
|
12100
|
+
const costUsd = asNumber2(cost);
|
|
12101
|
+
if (!input && !output && !cacheRead && !cacheWrite && !costUsd) {
|
|
12102
|
+
return void 0;
|
|
11614
12103
|
}
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
|
|
11619
|
-
|
|
11620
|
-
|
|
11621
|
-
|
|
12104
|
+
const extra = compactExtra3({
|
|
12105
|
+
reasoning_tokens: reasoning || void 0,
|
|
12106
|
+
cache_write_tokens: cacheWrite || void 0
|
|
12107
|
+
});
|
|
12108
|
+
return {
|
|
12109
|
+
prompt_tokens: input + cacheRead || void 0,
|
|
12110
|
+
completion_tokens: output || void 0,
|
|
12111
|
+
cached_tokens: cacheRead || void 0,
|
|
12112
|
+
cost_usd: costUsd || void 0,
|
|
12113
|
+
extra
|
|
12114
|
+
};
|
|
12115
|
+
}
|
|
12116
|
+
function metricsFromInfo(info) {
|
|
12117
|
+
return metricsFromTokens(info.tokens, info.cost);
|
|
12118
|
+
}
|
|
12119
|
+
function addFinalMetricTotals(totals, metrics) {
|
|
12120
|
+
if (!metrics) return;
|
|
12121
|
+
totals.total_prompt_tokens += metrics.prompt_tokens ?? 0;
|
|
12122
|
+
totals.total_completion_tokens += metrics.completion_tokens ?? 0;
|
|
12123
|
+
totals.total_cached_tokens += metrics.cached_tokens ?? 0;
|
|
12124
|
+
totals.total_cost_usd += metrics.cost_usd ?? 0;
|
|
12125
|
+
}
|
|
12126
|
+
function finalMetricsFromSteps2(steps) {
|
|
12127
|
+
const totals = {
|
|
12128
|
+
total_prompt_tokens: 0,
|
|
12129
|
+
total_completion_tokens: 0,
|
|
12130
|
+
total_cached_tokens: 0,
|
|
12131
|
+
total_cost_usd: 0,
|
|
12132
|
+
total_steps: steps.length,
|
|
12133
|
+
extra: {}
|
|
12134
|
+
};
|
|
12135
|
+
for (const step of steps) addFinalMetricTotals(totals, step.metrics);
|
|
12136
|
+
return {
|
|
12137
|
+
total_prompt_tokens: totals.total_prompt_tokens || void 0,
|
|
12138
|
+
total_completion_tokens: totals.total_completion_tokens || void 0,
|
|
12139
|
+
total_cached_tokens: totals.total_cached_tokens || void 0,
|
|
12140
|
+
total_cost_usd: totals.total_cost_usd || void 0,
|
|
12141
|
+
total_steps: steps.length
|
|
12142
|
+
};
|
|
12143
|
+
}
|
|
12144
|
+
function entryFromLine(line) {
|
|
12145
|
+
const info = asObject3(line.info);
|
|
12146
|
+
if (info) {
|
|
12147
|
+
return {
|
|
12148
|
+
info,
|
|
12149
|
+
parts: asArray(line.parts).flatMap((part) => {
|
|
12150
|
+
const obj = asObject3(part);
|
|
12151
|
+
return obj ? [obj] : [];
|
|
12152
|
+
})
|
|
12153
|
+
};
|
|
12154
|
+
}
|
|
12155
|
+
if (typeof line.role === "string" || typeof line.sessionID === "string") {
|
|
12156
|
+
return {
|
|
12157
|
+
info: line,
|
|
12158
|
+
parts: asArray(line.parts).flatMap((part) => {
|
|
12159
|
+
const obj = asObject3(part);
|
|
12160
|
+
return obj ? [obj] : [];
|
|
12161
|
+
})
|
|
12162
|
+
};
|
|
12163
|
+
}
|
|
12164
|
+
return null;
|
|
12165
|
+
}
|
|
12166
|
+
function entriesFromEventWrappers(lines) {
|
|
12167
|
+
const byMessage = /* @__PURE__ */ new Map();
|
|
12168
|
+
function getEntry(messageID, sessionID) {
|
|
12169
|
+
let entry = byMessage.get(messageID);
|
|
12170
|
+
if (!entry) {
|
|
12171
|
+
entry = { info: { id: messageID, sessionID }, parts: [] };
|
|
12172
|
+
byMessage.set(messageID, entry);
|
|
12173
|
+
}
|
|
12174
|
+
return entry;
|
|
12175
|
+
}
|
|
12176
|
+
for (const line of lines) {
|
|
12177
|
+
const type = line.type;
|
|
12178
|
+
const props = asObject3(line.properties) ?? line;
|
|
12179
|
+
if (type === "message.updated") {
|
|
12180
|
+
const info = asObject3(props.info);
|
|
12181
|
+
const id = typeof info?.id === "string" ? info.id : void 0;
|
|
12182
|
+
if (!info || !id) continue;
|
|
12183
|
+
const entry = getEntry(id, info.sessionID);
|
|
12184
|
+
entry.info = info;
|
|
12185
|
+
continue;
|
|
12186
|
+
}
|
|
12187
|
+
if (type === "message.part.updated") {
|
|
12188
|
+
const part = asObject3(props.part);
|
|
12189
|
+
const messageID = typeof part?.messageID === "string" && part.messageID || void 0;
|
|
12190
|
+
if (!part || !messageID) continue;
|
|
12191
|
+
const entry = getEntry(messageID, part.sessionID);
|
|
12192
|
+
const partID = typeof part.id === "string" ? part.id : void 0;
|
|
12193
|
+
const existingIndex = partID ? entry.parts.findIndex((p7) => p7.id === partID) : -1;
|
|
12194
|
+
if (existingIndex >= 0) entry.parts[existingIndex] = part;
|
|
12195
|
+
else entry.parts.push(part);
|
|
12196
|
+
}
|
|
12197
|
+
}
|
|
12198
|
+
return [...byMessage.values()];
|
|
12199
|
+
}
|
|
12200
|
+
function getSessionId(sessionId, exportInfo, entries) {
|
|
12201
|
+
if (sessionId) return sessionId;
|
|
12202
|
+
if (typeof exportInfo?.id === "string") return exportInfo.id;
|
|
12203
|
+
for (const entry of entries) {
|
|
12204
|
+
if (typeof entry.info.sessionID === "string") return entry.info.sessionID;
|
|
12205
|
+
}
|
|
12206
|
+
return "unknown";
|
|
12207
|
+
}
|
|
12208
|
+
function getAgentVersion(exportInfo) {
|
|
12209
|
+
return typeof exportInfo?.version === "string" && exportInfo.version || "unknown";
|
|
12210
|
+
}
|
|
12211
|
+
function sortEntries(entries) {
|
|
12212
|
+
return [...entries].sort((a, b) => {
|
|
12213
|
+
const at = asNumber2(asObject3(a.info.time)?.created) ?? 0;
|
|
12214
|
+
const bt = asNumber2(asObject3(b.info.time)?.created) ?? 0;
|
|
12215
|
+
return at - bt;
|
|
12216
|
+
});
|
|
12217
|
+
}
|
|
12218
|
+
function splitAssistantParts(parts) {
|
|
12219
|
+
let sawBoundary = false;
|
|
12220
|
+
const groups = [];
|
|
12221
|
+
let current = [];
|
|
12222
|
+
for (const part of parts) {
|
|
12223
|
+
const type = part.type;
|
|
12224
|
+
if (type === "step-start") {
|
|
12225
|
+
sawBoundary = true;
|
|
12226
|
+
if (current.length > 0) groups.push(current);
|
|
12227
|
+
current = [part];
|
|
12228
|
+
continue;
|
|
12229
|
+
}
|
|
12230
|
+
current.push(part);
|
|
12231
|
+
if (type === "step-finish") {
|
|
12232
|
+
sawBoundary = true;
|
|
12233
|
+
groups.push(current);
|
|
12234
|
+
current = [];
|
|
12235
|
+
}
|
|
12236
|
+
}
|
|
12237
|
+
if (current.length > 0) groups.push(current);
|
|
12238
|
+
return sawBoundary ? groups : [parts];
|
|
12239
|
+
}
|
|
12240
|
+
function textFromFilePart(part) {
|
|
12241
|
+
const filename = typeof part.filename === "string" ? part.filename : void 0;
|
|
12242
|
+
const url = typeof part.url === "string" ? part.url : void 0;
|
|
12243
|
+
const mime = typeof part.mime === "string" ? part.mime : void 0;
|
|
12244
|
+
const label = filename ?? url;
|
|
12245
|
+
if (!label) return void 0;
|
|
12246
|
+
return mime ? `[file:${mime}] ${label}` : `[file] ${label}`;
|
|
12247
|
+
}
|
|
12248
|
+
function buildUserStep(entry, stepId, defaultModelName) {
|
|
12249
|
+
const textParts = [];
|
|
12250
|
+
const extra = {};
|
|
12251
|
+
for (const part of entry.parts) {
|
|
12252
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
12253
|
+
if (part.text.trim()) textParts.push(part.text.trim());
|
|
12254
|
+
continue;
|
|
12255
|
+
}
|
|
12256
|
+
if (part.type === "file") {
|
|
12257
|
+
const fileText = textFromFilePart(part);
|
|
12258
|
+
if (fileText) textParts.push(fileText);
|
|
12259
|
+
}
|
|
12260
|
+
}
|
|
12261
|
+
const message = textParts.join("\n\n").trim();
|
|
12262
|
+
if (!message) return null;
|
|
12263
|
+
if (entry.info.agent) extra.agent = entry.info.agent;
|
|
12264
|
+
if (entry.info.tools) extra.tools = entry.info.tools;
|
|
12265
|
+
return {
|
|
12266
|
+
step_id: stepId,
|
|
12267
|
+
timestamp: timeFromObject(entry.info.time),
|
|
12268
|
+
source: "user",
|
|
12269
|
+
message,
|
|
12270
|
+
model_name: defaultModelName,
|
|
12271
|
+
extra: compactExtra3(extra)
|
|
12272
|
+
};
|
|
12273
|
+
}
|
|
12274
|
+
function buildAgentStep(parts, info, stepId, defaultModelName, fallbackMetrics) {
|
|
12275
|
+
const textParts = [];
|
|
12276
|
+
const reasoningParts = [];
|
|
12277
|
+
const toolCalls = [];
|
|
12278
|
+
const observationResults = [];
|
|
12279
|
+
const extra = {};
|
|
12280
|
+
let metrics;
|
|
12281
|
+
let timestamp = timeFromObject(info.time);
|
|
12282
|
+
for (const part of parts) {
|
|
12283
|
+
if (!timestamp) timestamp = timeFromObject(part.time);
|
|
12284
|
+
switch (part.type) {
|
|
12285
|
+
case "text":
|
|
12286
|
+
if (typeof part.text === "string" && part.text.trim()) {
|
|
12287
|
+
textParts.push(part.text.trim());
|
|
12288
|
+
}
|
|
12289
|
+
break;
|
|
12290
|
+
case "reasoning":
|
|
12291
|
+
if (typeof part.text === "string" && part.text.trim()) {
|
|
12292
|
+
reasoningParts.push(part.text.trim());
|
|
12293
|
+
}
|
|
12294
|
+
break;
|
|
12295
|
+
case "file": {
|
|
12296
|
+
const fileText = textFromFilePart(part);
|
|
12297
|
+
if (fileText) textParts.push(fileText);
|
|
12298
|
+
break;
|
|
12299
|
+
}
|
|
12300
|
+
case "tool": {
|
|
12301
|
+
const state = asObject3(part.state) ?? {};
|
|
12302
|
+
const callID = typeof part.callID === "string" && part.callID || typeof part.id === "string" && part.id || "";
|
|
12303
|
+
const toolName = typeof part.tool === "string" && part.tool || "tool";
|
|
12304
|
+
const input = argsFromUnknown(state.input);
|
|
12305
|
+
toolCalls.push({
|
|
12306
|
+
tool_call_id: callID,
|
|
12307
|
+
function_name: toolName,
|
|
12308
|
+
arguments: input
|
|
12309
|
+
});
|
|
12310
|
+
const output = state.output ?? state.error;
|
|
12311
|
+
if (output !== void 0 && output !== null) {
|
|
12312
|
+
observationResults.push({
|
|
12313
|
+
source_call_id: callID || void 0,
|
|
12314
|
+
content: stringify2(output)
|
|
12315
|
+
});
|
|
12316
|
+
}
|
|
12317
|
+
if (state.status) extra.status = state.status;
|
|
12318
|
+
if (state.metadata) extra.tool_metadata = state.metadata;
|
|
12319
|
+
if (part.metadata) extra.part_metadata = part.metadata;
|
|
12320
|
+
break;
|
|
12321
|
+
}
|
|
12322
|
+
case "step-finish":
|
|
12323
|
+
metrics = metricsFromTokens(part.tokens, part.cost) ?? metrics;
|
|
12324
|
+
if (part.reason) extra.finish_reason = part.reason;
|
|
12325
|
+
if (part.snapshot) extra.snapshot = part.snapshot;
|
|
12326
|
+
break;
|
|
12327
|
+
case "patch":
|
|
12328
|
+
extra.patches = [...asArray(extra.patches), part];
|
|
12329
|
+
break;
|
|
12330
|
+
case "agent":
|
|
12331
|
+
if (part.name) extra.agent = part.name;
|
|
12332
|
+
break;
|
|
12333
|
+
case "retry":
|
|
12334
|
+
extra.retry = part;
|
|
12335
|
+
break;
|
|
12336
|
+
}
|
|
12337
|
+
}
|
|
12338
|
+
metrics = metrics ?? fallbackMetrics;
|
|
12339
|
+
if (textParts.length === 0 && reasoningParts.length === 0 && toolCalls.length === 0 && !metrics) {
|
|
12340
|
+
return null;
|
|
12341
|
+
}
|
|
12342
|
+
const observation = observationResults.length > 0 ? { results: observationResults } : void 0;
|
|
12343
|
+
const step = {
|
|
12344
|
+
step_id: stepId,
|
|
12345
|
+
timestamp,
|
|
12346
|
+
source: "agent",
|
|
12347
|
+
message: textParts.length > 0 ? textParts.join("\n\n") : "(tool use)",
|
|
12348
|
+
model_name: modelNameFromInfo(info) ?? defaultModelName
|
|
12349
|
+
};
|
|
12350
|
+
if (reasoningParts.length > 0)
|
|
12351
|
+
step.reasoning_content = reasoningParts.join("\n\n");
|
|
12352
|
+
if (toolCalls.length > 0) step.tool_calls = toolCalls;
|
|
12353
|
+
if (observation) step.observation = observation;
|
|
12354
|
+
if (metrics) step.metrics = metrics;
|
|
12355
|
+
const compactedExtra = compactExtra3(extra);
|
|
12356
|
+
if (compactedExtra) step.extra = compactedExtra;
|
|
12357
|
+
return step;
|
|
12358
|
+
}
|
|
12359
|
+
function buildUnavailableAgentStep(info, stepId, defaultModelName) {
|
|
12360
|
+
const metrics = metricsFromInfo(info);
|
|
12361
|
+
if (!metrics && !modelNameFromInfo(info) && !info.error && !info.finish) {
|
|
12362
|
+
return null;
|
|
12363
|
+
}
|
|
12364
|
+
return {
|
|
12365
|
+
step_id: stepId,
|
|
12366
|
+
timestamp: timeFromObject(info.time),
|
|
12367
|
+
source: "agent",
|
|
12368
|
+
message: "(message unavailable)",
|
|
12369
|
+
model_name: modelNameFromInfo(info) ?? defaultModelName,
|
|
12370
|
+
metrics,
|
|
12371
|
+
extra: compactExtra3({
|
|
12372
|
+
content_unavailable: true,
|
|
12373
|
+
finish_reason: info.finish,
|
|
12374
|
+
error: info.error
|
|
12375
|
+
})
|
|
12376
|
+
};
|
|
12377
|
+
}
|
|
12378
|
+
function convertMessageEntriesToTrajectory(entries, sessionId, exportInfo) {
|
|
12379
|
+
if (entries.length === 0) return null;
|
|
12380
|
+
const orderedEntries = sortEntries(entries);
|
|
12381
|
+
const defaultModelName = orderedEntries.map((entry) => modelNameFromInfo(entry.info)).find((model) => typeof model === "string");
|
|
12382
|
+
const steps = [];
|
|
12383
|
+
for (const entry of orderedEntries) {
|
|
12384
|
+
const role = entry.info.role;
|
|
12385
|
+
if (role === "user") {
|
|
12386
|
+
const step = buildUserStep(entry, steps.length + 1, defaultModelName);
|
|
12387
|
+
if (step) steps.push(step);
|
|
12388
|
+
continue;
|
|
12389
|
+
}
|
|
12390
|
+
if (role !== "assistant") continue;
|
|
12391
|
+
if (entry.parts.length === 0) {
|
|
12392
|
+
const step = buildUnavailableAgentStep(
|
|
12393
|
+
entry.info,
|
|
12394
|
+
steps.length + 1,
|
|
12395
|
+
defaultModelName
|
|
12396
|
+
);
|
|
12397
|
+
if (step) steps.push(step);
|
|
12398
|
+
continue;
|
|
12399
|
+
}
|
|
12400
|
+
const groups = splitAssistantParts(entry.parts);
|
|
12401
|
+
const hasPartMetrics = groups.some(
|
|
12402
|
+
(group) => group.some((part) => part.type === "step-finish")
|
|
12403
|
+
);
|
|
12404
|
+
const fallbackMetrics = hasPartMetrics ? void 0 : metricsFromInfo(entry.info);
|
|
12405
|
+
for (let i = 0; i < groups.length; i++) {
|
|
12406
|
+
const step = buildAgentStep(
|
|
12407
|
+
groups[i],
|
|
12408
|
+
entry.info,
|
|
12409
|
+
steps.length + 1,
|
|
12410
|
+
defaultModelName,
|
|
12411
|
+
i === 0 ? fallbackMetrics : void 0
|
|
12412
|
+
);
|
|
12413
|
+
if (step) steps.push(step);
|
|
12414
|
+
}
|
|
12415
|
+
}
|
|
12416
|
+
if (steps.length === 0) return null;
|
|
12417
|
+
return {
|
|
12418
|
+
schema_version: ATIF_VERSION,
|
|
12419
|
+
session_id: getSessionId(sessionId, exportInfo, orderedEntries),
|
|
12420
|
+
agent: {
|
|
12421
|
+
name: "opencode",
|
|
12422
|
+
version: getAgentVersion(exportInfo),
|
|
12423
|
+
model_name: defaultModelName
|
|
12424
|
+
},
|
|
12425
|
+
steps,
|
|
12426
|
+
final_metrics: finalMetricsFromSteps2(steps)
|
|
12427
|
+
};
|
|
12428
|
+
}
|
|
12429
|
+
function convertRunEventsToTrajectory(events, sessionId) {
|
|
12430
|
+
const session = sessionId ?? (events.map((event) => event.sessionID).find((sid) => typeof sid === "string") || "unknown");
|
|
12431
|
+
const turns = [];
|
|
12432
|
+
let current = null;
|
|
12433
|
+
for (const event of events) {
|
|
12434
|
+
const type = event.type;
|
|
12435
|
+
if (type === "step_start") {
|
|
12436
|
+
current = { parts: [], timestamp: event.timestamp };
|
|
12437
|
+
continue;
|
|
12438
|
+
}
|
|
12439
|
+
if (type === "step_finish") {
|
|
12440
|
+
if (current) {
|
|
12441
|
+
current.finish = asObject3(event.part) ?? {};
|
|
12442
|
+
turns.push(current);
|
|
12443
|
+
current = null;
|
|
12444
|
+
}
|
|
12445
|
+
continue;
|
|
12446
|
+
}
|
|
12447
|
+
if (current && (type === "text" || type === "reasoning" || type === "tool_use")) {
|
|
12448
|
+
const part = asObject3(event.part);
|
|
12449
|
+
if (part) current.parts.push(part);
|
|
12450
|
+
}
|
|
12451
|
+
}
|
|
12452
|
+
const steps = [];
|
|
12453
|
+
for (const turn of turns) {
|
|
12454
|
+
const parts = [...turn.parts];
|
|
12455
|
+
if (turn.finish) {
|
|
12456
|
+
parts.push({ ...turn.finish, type: "step-finish" });
|
|
12457
|
+
}
|
|
12458
|
+
const step = buildAgentStep(
|
|
12459
|
+
parts,
|
|
12460
|
+
{ time: { created: turn.timestamp } },
|
|
12461
|
+
steps.length + 1,
|
|
12462
|
+
void 0
|
|
12463
|
+
);
|
|
12464
|
+
if (step) steps.push(step);
|
|
12465
|
+
}
|
|
12466
|
+
if (steps.length === 0) return null;
|
|
12467
|
+
return {
|
|
12468
|
+
schema_version: ATIF_VERSION,
|
|
12469
|
+
session_id: session,
|
|
12470
|
+
agent: {
|
|
12471
|
+
name: "opencode",
|
|
12472
|
+
version: "unknown"
|
|
12473
|
+
},
|
|
12474
|
+
steps,
|
|
12475
|
+
final_metrics: finalMetricsFromSteps2(steps)
|
|
12476
|
+
};
|
|
12477
|
+
}
|
|
12478
|
+
function isRunEvent(lines) {
|
|
12479
|
+
return lines.some(
|
|
12480
|
+
(line) => ["step_start", "step_finish", "text", "reasoning", "tool_use"].includes(
|
|
12481
|
+
String(line.type ?? "")
|
|
12482
|
+
)
|
|
12483
|
+
);
|
|
12484
|
+
}
|
|
12485
|
+
function convertOpenCodeToTrajectory(content, sessionId) {
|
|
12486
|
+
const trimmed = content.trim();
|
|
12487
|
+
if (!trimmed) return null;
|
|
12488
|
+
try {
|
|
12489
|
+
const parsed = JSON.parse(trimmed);
|
|
12490
|
+
const parsedObj = asObject3(parsed);
|
|
12491
|
+
const messages = asArray(parsedObj?.messages);
|
|
12492
|
+
if (parsedObj && messages.length > 0) {
|
|
12493
|
+
const entries2 = messages.flatMap((message) => {
|
|
12494
|
+
const entry = entryFromLine(asObject3(message) ?? {});
|
|
12495
|
+
return entry ? [entry] : [];
|
|
12496
|
+
});
|
|
12497
|
+
return convertMessageEntriesToTrajectory(
|
|
12498
|
+
entries2,
|
|
12499
|
+
sessionId,
|
|
12500
|
+
asObject3(parsedObj.info)
|
|
12501
|
+
);
|
|
12502
|
+
}
|
|
12503
|
+
} catch {
|
|
12504
|
+
}
|
|
12505
|
+
const lines = parseJsonLines2(content);
|
|
12506
|
+
if (lines.length === 0) return null;
|
|
12507
|
+
if (isRunEvent(lines)) return convertRunEventsToTrajectory(lines, sessionId);
|
|
12508
|
+
const wrappedEntries = entriesFromEventWrappers(lines);
|
|
12509
|
+
const entries = wrappedEntries.length > 0 ? wrappedEntries : lines.flatMap((line) => {
|
|
12510
|
+
const entry = entryFromLine(line);
|
|
12511
|
+
return entry ? [entry] : [];
|
|
12512
|
+
});
|
|
12513
|
+
return convertMessageEntriesToTrajectory(entries, sessionId);
|
|
12514
|
+
}
|
|
12515
|
+
|
|
12516
|
+
// src/normalizer/index.ts
|
|
12517
|
+
function normalizeContent(sourceName, content, sessionId) {
|
|
12518
|
+
switch (sourceName) {
|
|
12519
|
+
case "claude":
|
|
12520
|
+
return convertClaudeToTrajectory(content, sessionId);
|
|
12521
|
+
case "codex":
|
|
12522
|
+
return convertCodexToTrajectory(content, sessionId);
|
|
12523
|
+
case "cursor":
|
|
12524
|
+
return convertCursorToTrajectory(content, sessionId);
|
|
12525
|
+
case "opencode":
|
|
12526
|
+
return convertOpenCodeToTrajectory(content, sessionId);
|
|
12527
|
+
case "copilot-chat":
|
|
12528
|
+
return convertCopilotChatToTrajectory(content, sessionId);
|
|
12529
|
+
default:
|
|
12530
|
+
return null;
|
|
12531
|
+
}
|
|
12532
|
+
}
|
|
12533
|
+
var NormalizeMiddleware = class {
|
|
12534
|
+
name = "normalize";
|
|
12535
|
+
async process(group) {
|
|
12536
|
+
const newFiles = [];
|
|
12537
|
+
for (const file of group.files) {
|
|
12538
|
+
newFiles.push(file);
|
|
12539
|
+
if (!file.absolutePath.endsWith(".jsonl")) continue;
|
|
12540
|
+
if (!["claude", "codex", "cursor", "opencode", "copilot-chat"].includes(
|
|
12541
|
+
file.sourceName
|
|
12542
|
+
))
|
|
12543
|
+
continue;
|
|
12544
|
+
const content = file.content ? file.content.toString("utf-8") : null;
|
|
12545
|
+
if (!content) continue;
|
|
12546
|
+
const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 : path9.basename(file.absolutePath, ".jsonl"));
|
|
12547
|
+
try {
|
|
12548
|
+
const trajectory = normalizeContent(
|
|
12549
|
+
file.sourceName,
|
|
12550
|
+
content,
|
|
12551
|
+
sessionId
|
|
12552
|
+
);
|
|
12553
|
+
if (!trajectory) continue;
|
|
12554
|
+
const json = JSON.stringify(
|
|
12555
|
+
excludeNone(trajectory),
|
|
12556
|
+
null,
|
|
12557
|
+
2
|
|
12558
|
+
);
|
|
12559
|
+
const atifPath = file.absolutePath.replace(/\.jsonl$/, ".atif.json");
|
|
12560
|
+
newFiles.push({
|
|
12561
|
+
sourceName: file.sourceName,
|
|
12562
|
+
absolutePath: atifPath,
|
|
12563
|
+
repoPath: file.repoPath,
|
|
12564
|
+
metadata: { ...file.metadata, isAtif: true },
|
|
12565
|
+
content: Buffer.from(json, "utf-8")
|
|
12566
|
+
});
|
|
12567
|
+
} catch {
|
|
12568
|
+
}
|
|
12569
|
+
}
|
|
12570
|
+
return { ...group, files: newFiles };
|
|
12571
|
+
}
|
|
12572
|
+
};
|
|
12573
|
+
|
|
12574
|
+
// src/outputs/platform.ts
|
|
12575
|
+
import { PassThrough } from "stream";
|
|
12576
|
+
import archiver from "archiver";
|
|
12577
|
+
|
|
12578
|
+
// src/outputs/archive.ts
|
|
11622
12579
|
import os4 from "os";
|
|
11623
12580
|
import path10 from "path";
|
|
11624
12581
|
function getSourceBaseDir(sourceName) {
|
|
@@ -11843,6 +12800,24 @@ function resolveSourceTool(payload) {
|
|
|
11843
12800
|
if (payload.hook_event_name === "Stop") return "codex";
|
|
11844
12801
|
return "claude";
|
|
11845
12802
|
}
|
|
12803
|
+
async function selfHealHook(repoRoot, tool) {
|
|
12804
|
+
if (process.env.HILLCLIMB_SKIP_HOOK_SELF_HEAL === "1") return;
|
|
12805
|
+
try {
|
|
12806
|
+
const result = await healHookForTool(repoRoot, tool);
|
|
12807
|
+
if (result.skipped) {
|
|
12808
|
+
appendLog("warn", `self-heal: skipped ${tool} hook (${result.skipped})`);
|
|
12809
|
+
return;
|
|
12810
|
+
}
|
|
12811
|
+
if (result.changed) {
|
|
12812
|
+
appendLog("info", `self-heal: updated ${tool} hook`);
|
|
12813
|
+
}
|
|
12814
|
+
} catch (err) {
|
|
12815
|
+
appendLog(
|
|
12816
|
+
"warn",
|
|
12817
|
+
`self-heal: failed to repair ${tool} hook: ${err instanceof Error ? err.message : String(err)}`
|
|
12818
|
+
);
|
|
12819
|
+
}
|
|
12820
|
+
}
|
|
11846
12821
|
function resolveCursorTranscriptPath(payload) {
|
|
11847
12822
|
const id = payload.conversation_id ?? payload.session_id;
|
|
11848
12823
|
const workspace = payload.workspace_roots?.[0];
|
|
@@ -11874,6 +12849,16 @@ async function runUploadInner(payload) {
|
|
|
11874
12849
|
);
|
|
11875
12850
|
return;
|
|
11876
12851
|
}
|
|
12852
|
+
const match = await findProjectForCwd(cwd);
|
|
12853
|
+
if (!match) {
|
|
12854
|
+
appendLog(
|
|
12855
|
+
"warn",
|
|
12856
|
+
`Skipping session ${sessionId}: no hillclimb config for cwd ${cwd}. Run \`npx hillclimb\` in the repo.`
|
|
12857
|
+
);
|
|
12858
|
+
return;
|
|
12859
|
+
}
|
|
12860
|
+
const { repoRoot, config } = match;
|
|
12861
|
+
await selfHealHook(repoRoot, sourceTool);
|
|
11877
12862
|
const transcriptResolved = path12.resolve(transcriptPath);
|
|
11878
12863
|
try {
|
|
11879
12864
|
const stat = await fs9.promises.stat(transcriptResolved);
|
|
@@ -11891,14 +12876,6 @@ async function runUploadInner(payload) {
|
|
|
11891
12876
|
);
|
|
11892
12877
|
return;
|
|
11893
12878
|
}
|
|
11894
|
-
const match = await findProjectForCwd(cwd);
|
|
11895
|
-
if (!match) {
|
|
11896
|
-
appendLog(
|
|
11897
|
-
"warn",
|
|
11898
|
-
`Skipping session ${sessionId}: no hillclimb config for cwd ${cwd}. Run \`npx hillclimb\` in the repo.`
|
|
11899
|
-
);
|
|
11900
|
-
return;
|
|
11901
|
-
}
|
|
11902
12879
|
if (!await hasAssistantMessage(transcriptResolved)) {
|
|
11903
12880
|
appendLog(
|
|
11904
12881
|
"info",
|
|
@@ -11906,7 +12883,6 @@ async function runUploadInner(payload) {
|
|
|
11906
12883
|
);
|
|
11907
12884
|
return;
|
|
11908
12885
|
}
|
|
11909
|
-
const { repoRoot, config } = match;
|
|
11910
12886
|
await uploadSession({
|
|
11911
12887
|
sessionId,
|
|
11912
12888
|
transcriptPath: transcriptResolved,
|
|
@@ -12108,6 +13084,7 @@ import crypto2 from "crypto";
|
|
|
12108
13084
|
|
|
12109
13085
|
// src/git-traces/handlers.ts
|
|
12110
13086
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
13087
|
+
import path15 from "path";
|
|
12111
13088
|
|
|
12112
13089
|
// src/git-traces/git-ops.ts
|
|
12113
13090
|
import { execFileSync } from "child_process";
|
|
@@ -12362,12 +13339,34 @@ function detectTransitionKind(repoRoot, prevHeadSha, nextHeadSha) {
|
|
|
12362
13339
|
return "branch-switch";
|
|
12363
13340
|
}
|
|
12364
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
|
+
}
|
|
12365
13364
|
function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind, startedAt) {
|
|
12366
13365
|
const headSha = safeGit(repoRoot, ["rev-parse", "HEAD"]) ?? "unknown";
|
|
12367
13366
|
const branch = safeGit(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? null;
|
|
12368
13367
|
const remoteUrl = safeGit(repoRoot, ["config", "--get", "remote.origin.url"]) ?? null;
|
|
12369
|
-
const statusRaw =
|
|
12370
|
-
const dirtyFiles = statusRaw
|
|
13368
|
+
const statusRaw = safeGitBuffer(repoRoot, ["status", "--porcelain=v1", "-z"]) ?? Buffer.alloc(0);
|
|
13369
|
+
const dirtyFiles = parseDirtyFilesFromStatus(statusRaw);
|
|
12371
13370
|
const authorName = safeGit(repoRoot, ["config", "user.name"]) ?? "";
|
|
12372
13371
|
const authorEmail = safeGit(repoRoot, ["config", "user.email"]) ?? "";
|
|
12373
13372
|
const commits = transitionKind === "commit" && prevHeadSha && headSha !== "unknown" ? enumerateCommits(repoRoot, prevHeadSha, headSha) : void 0;
|
|
@@ -12478,9 +13477,9 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
12478
13477
|
oldPath
|
|
12479
13478
|
});
|
|
12480
13479
|
} else {
|
|
12481
|
-
const
|
|
12482
|
-
indexByPath.set(
|
|
12483
|
-
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 });
|
|
12484
13483
|
}
|
|
12485
13484
|
}
|
|
12486
13485
|
for (const line of numstat.split("\n")) {
|
|
@@ -12511,6 +13510,19 @@ function safeGit(repoRoot, args) {
|
|
|
12511
13510
|
return null;
|
|
12512
13511
|
}
|
|
12513
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
|
+
}
|
|
12514
13526
|
function cleanupSessionRefs(repoRoot, sessionId) {
|
|
12515
13527
|
for (const prefix of [
|
|
12516
13528
|
`refs/hillclimb/baseline/${sessionId}`,
|
|
@@ -12538,20 +13550,24 @@ import fs11 from "fs";
|
|
|
12538
13550
|
import os7 from "os";
|
|
12539
13551
|
import path14 from "path";
|
|
12540
13552
|
var CURRENT_SCHEMA_VERSION = 3;
|
|
12541
|
-
var
|
|
12542
|
-
|
|
12543
|
-
|
|
12544
|
-
|
|
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`);
|
|
12545
13564
|
}
|
|
12546
13565
|
function lockFileForRepo(repoRoot, tool) {
|
|
12547
13566
|
return `${stateFileForRepo(repoRoot, tool)}.lock`;
|
|
12548
13567
|
}
|
|
12549
|
-
async function
|
|
13568
|
+
async function readStateFile(file) {
|
|
12550
13569
|
try {
|
|
12551
|
-
const raw = await fs11.promises.readFile(
|
|
12552
|
-
stateFileForRepo(repoRoot, tool),
|
|
12553
|
-
"utf-8"
|
|
12554
|
-
);
|
|
13570
|
+
const raw = await fs11.promises.readFile(file, "utf-8");
|
|
12555
13571
|
const parsed = JSON.parse(raw);
|
|
12556
13572
|
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) {
|
|
12557
13573
|
return null;
|
|
@@ -12561,42 +13577,130 @@ async function readSessionState(repoRoot, tool) {
|
|
|
12561
13577
|
return null;
|
|
12562
13578
|
}
|
|
12563
13579
|
}
|
|
12564
|
-
async function
|
|
12565
|
-
|
|
12566
|
-
await fs11.promises.mkdir(STATE_DIR, { recursive: true, mode: 448 });
|
|
12567
|
-
const tmp = `${file}.tmp`;
|
|
12568
|
-
await fs11.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
12569
|
-
mode: 384
|
|
12570
|
-
});
|
|
12571
|
-
await fs11.promises.rename(tmp, file);
|
|
12572
|
-
}
|
|
12573
|
-
async function deleteSessionState(repoRoot, tool) {
|
|
13580
|
+
async function listScopedSessionStates(repoRoot, tool) {
|
|
13581
|
+
let entries;
|
|
12574
13582
|
try {
|
|
12575
|
-
await fs11.promises.
|
|
13583
|
+
entries = await fs11.promises.readdir(stateDir(), { withFileTypes: true });
|
|
12576
13584
|
} catch {
|
|
13585
|
+
return [];
|
|
12577
13586
|
}
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
12582
|
-
|
|
12583
|
-
|
|
12584
|
-
|
|
12585
|
-
|
|
12586
|
-
fs11.constants.O_CREAT | fs11.constants.O_EXCL | fs11.constants.O_WRONLY
|
|
12587
|
-
);
|
|
12588
|
-
await fd.write(String(process.pid));
|
|
12589
|
-
await fd.close();
|
|
12590
|
-
return fd.fd;
|
|
12591
|
-
} catch (err) {
|
|
12592
|
-
if (err.code === "EEXIST" && i < retries - 1) {
|
|
12593
|
-
await new Promise((r) => setTimeout(r, delayMs));
|
|
12594
|
-
continue;
|
|
12595
|
-
}
|
|
12596
|
-
throw err;
|
|
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;
|
|
12597
13595
|
}
|
|
12598
|
-
|
|
12599
|
-
|
|
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
|
+
}
|
|
13651
|
+
async function writeSessionState(state, tool) {
|
|
13652
|
+
const file = stateFileForRepo(state.repoRoot, tool, state.sessionId);
|
|
13653
|
+
await fs11.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
|
|
13654
|
+
const tmp = `${file}.tmp`;
|
|
13655
|
+
await fs11.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
13656
|
+
mode: 384
|
|
13657
|
+
});
|
|
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
|
+
}
|
|
13664
|
+
}
|
|
13665
|
+
async function deleteStateFile(file) {
|
|
13666
|
+
try {
|
|
13667
|
+
await fs11.promises.unlink(file);
|
|
13668
|
+
} catch {
|
|
13669
|
+
}
|
|
13670
|
+
}
|
|
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) {
|
|
13684
|
+
const lockPath = lockFileForRepo(repoRoot, tool);
|
|
13685
|
+
await fs11.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
|
|
13686
|
+
for (let i = 0; i < retries; i++) {
|
|
13687
|
+
try {
|
|
13688
|
+
const fd = await fs11.promises.open(
|
|
13689
|
+
lockPath,
|
|
13690
|
+
fs11.constants.O_CREAT | fs11.constants.O_EXCL | fs11.constants.O_WRONLY
|
|
13691
|
+
);
|
|
13692
|
+
await fd.write(String(process.pid));
|
|
13693
|
+
await fd.close();
|
|
13694
|
+
return fd.fd;
|
|
13695
|
+
} catch (err) {
|
|
13696
|
+
if (err.code === "EEXIST" && i < retries - 1) {
|
|
13697
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
13698
|
+
continue;
|
|
13699
|
+
}
|
|
13700
|
+
throw err;
|
|
13701
|
+
}
|
|
13702
|
+
}
|
|
13703
|
+
throw new Error(`Failed to acquire lock after ${retries} retries`);
|
|
12600
13704
|
}
|
|
12601
13705
|
async function releaseLock(repoRoot, tool) {
|
|
12602
13706
|
try {
|
|
@@ -12606,9 +13710,10 @@ async function releaseLock(repoRoot, tool) {
|
|
|
12606
13710
|
}
|
|
12607
13711
|
|
|
12608
13712
|
// src/git-traces/handlers.ts
|
|
12609
|
-
var CLI_VERSION = "0.
|
|
13713
|
+
var CLI_VERSION = "0.2.0";
|
|
12610
13714
|
var GIT_TRACES_SLUG = "git-traces";
|
|
12611
13715
|
var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
|
|
13716
|
+
var STALE_SCOPED_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
12612
13717
|
function formatEpochSeconds2(date) {
|
|
12613
13718
|
return String(Math.floor(date.getTime() / 1e3));
|
|
12614
13719
|
}
|
|
@@ -12619,6 +13724,16 @@ var TOOL_LABELS = {
|
|
|
12619
13724
|
"copilot-chat": "GitHub Copilot Chat",
|
|
12620
13725
|
opencode: "opencode"
|
|
12621
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
|
+
}
|
|
12622
13737
|
function resolveCwd(payload) {
|
|
12623
13738
|
return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
|
|
12624
13739
|
}
|
|
@@ -12678,27 +13793,43 @@ function canUploadEpochBaselineArtifacts(epoch, artifacts) {
|
|
|
12678
13793
|
}
|
|
12679
13794
|
function pinEpochBaseline(repoRoot, sessionId, epoch) {
|
|
12680
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
|
+
]);
|
|
12681
13809
|
pinRef(
|
|
12682
13810
|
repoRoot,
|
|
12683
|
-
`refs/hillclimb/baseline/${sessionId}/${
|
|
12684
|
-
|
|
13811
|
+
`refs/hillclimb/baseline/${sessionId}/${prefix}/tree`,
|
|
13812
|
+
commit
|
|
12685
13813
|
);
|
|
12686
|
-
return { baselineSha, headSha: captureHeadSha(repoRoot) };
|
|
12687
13814
|
}
|
|
12688
|
-
function
|
|
13815
|
+
function freezeEpochBaseline(params) {
|
|
12689
13816
|
const {
|
|
12690
13817
|
repoRoot,
|
|
12691
13818
|
sessionId,
|
|
12692
13819
|
tool,
|
|
12693
13820
|
epoch,
|
|
12694
|
-
baselineSha,
|
|
12695
13821
|
prevHeadSha,
|
|
12696
13822
|
transitionKind,
|
|
12697
13823
|
startedAt
|
|
12698
13824
|
} = params;
|
|
12699
13825
|
const prefix = epochPrefix(epoch);
|
|
12700
13826
|
try {
|
|
12701
|
-
const
|
|
13827
|
+
const { baselineSha, headSha } = pinEpochBaseline(
|
|
13828
|
+
repoRoot,
|
|
13829
|
+
sessionId,
|
|
13830
|
+
epoch
|
|
13831
|
+
);
|
|
13832
|
+
const baselineMetadata = buildBaselineMetadata(
|
|
12702
13833
|
repoRoot,
|
|
12703
13834
|
sessionId,
|
|
12704
13835
|
tool,
|
|
@@ -12710,13 +13841,29 @@ function buildEpochBaselineArtifacts(params) {
|
|
|
12710
13841
|
startedAt
|
|
12711
13842
|
);
|
|
12712
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 {
|
|
12713
13858
|
const bundleBuffer = createBundleFromTree(
|
|
12714
13859
|
repoRoot,
|
|
12715
13860
|
baselineTreeSha,
|
|
12716
13861
|
`${sessionId}-${prefix}`,
|
|
12717
13862
|
`baseline ${prefix}`
|
|
12718
13863
|
);
|
|
12719
|
-
const metadataBuffer = Buffer.from(
|
|
13864
|
+
const metadataBuffer = Buffer.from(
|
|
13865
|
+
JSON.stringify(baselineMetadata, null, 2)
|
|
13866
|
+
);
|
|
12720
13867
|
return { baselineTreeSha, metadataBuffer, bundleBuffer };
|
|
12721
13868
|
} catch (err) {
|
|
12722
13869
|
appendLog(
|
|
@@ -12726,6 +13873,46 @@ function buildEpochBaselineArtifacts(params) {
|
|
|
12726
13873
|
return null;
|
|
12727
13874
|
}
|
|
12728
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
|
+
}
|
|
12729
13916
|
async function uploadEpochBaselineArtifacts(params) {
|
|
12730
13917
|
const { client, contributionId, epoch, artifacts } = params;
|
|
12731
13918
|
const prefix = epochPrefix(epoch);
|
|
@@ -12746,6 +13933,34 @@ async function uploadEpochBaselineArtifacts(params) {
|
|
|
12746
13933
|
artifacts.metadataBuffer
|
|
12747
13934
|
);
|
|
12748
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
|
+
}
|
|
12749
13964
|
async function uploadEpochBaseline(params) {
|
|
12750
13965
|
const artifacts = buildEpochBaselineArtifacts(params);
|
|
12751
13966
|
if (!artifacts) return null;
|
|
@@ -12769,45 +13984,115 @@ async function openEpoch(params) {
|
|
|
12769
13984
|
if (!uploaded) return null;
|
|
12770
13985
|
return { baselineSha, baselineTreeSha: uploaded.baselineTreeSha, headSha };
|
|
12771
13986
|
}
|
|
12772
|
-
async function initializeSession(
|
|
12773
|
-
const
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
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;
|
|
12782
13999
|
const state = {
|
|
12783
14000
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
12784
14001
|
sessionId,
|
|
12785
14002
|
contributionId: null,
|
|
12786
|
-
baselineSha,
|
|
12787
|
-
baselineTreeSha:
|
|
12788
|
-
|
|
12789
|
-
|
|
12790
|
-
|
|
14003
|
+
baselineSha: frozen.baselineSha,
|
|
14004
|
+
baselineTreeSha: frozen.baselineTreeSha,
|
|
14005
|
+
baselineMetadata: frozen.baselineMetadata,
|
|
14006
|
+
lastSnapshotSha: frozen.baselineSha,
|
|
14007
|
+
lastSnapshotTreeSha: frozen.baselineTreeSha,
|
|
14008
|
+
startedAt,
|
|
12791
14009
|
epoch: 1,
|
|
12792
14010
|
turnCount: 0,
|
|
12793
|
-
headSha,
|
|
12794
|
-
repoRoot
|
|
14011
|
+
headSha: frozen.headSha,
|
|
14012
|
+
repoRoot
|
|
12795
14013
|
};
|
|
12796
14014
|
await writeSessionState(state, tool);
|
|
12797
14015
|
appendLog(
|
|
12798
14016
|
"info",
|
|
12799
|
-
`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)`
|
|
12800
14018
|
);
|
|
12801
14019
|
return state;
|
|
12802
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
|
+
}
|
|
12803
14084
|
async function handleSessionStart(payload, tool) {
|
|
12804
14085
|
const cwd = resolveCwd(payload);
|
|
12805
14086
|
if (!cwd) {
|
|
12806
14087
|
appendLog("warn", "git-traces: no cwd in payload, skipping");
|
|
12807
14088
|
return;
|
|
12808
14089
|
}
|
|
12809
|
-
|
|
12810
|
-
|
|
14090
|
+
const startingProject = await findProjectForCwd(cwd);
|
|
14091
|
+
if (!startingProject) {
|
|
14092
|
+
appendLog(
|
|
14093
|
+
"warn",
|
|
14094
|
+
"git-traces: no hillclimb project config found, skipping"
|
|
14095
|
+
);
|
|
12811
14096
|
return;
|
|
12812
14097
|
}
|
|
12813
14098
|
const sessionId = resolveSessionId(payload);
|
|
@@ -12815,115 +14100,146 @@ async function handleSessionStart(payload, tool) {
|
|
|
12815
14100
|
appendLog("warn", "git-traces: no session_id in payload, skipping");
|
|
12816
14101
|
return;
|
|
12817
14102
|
}
|
|
12818
|
-
await
|
|
12819
|
-
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
|
|
12823
|
-
|
|
12824
|
-
|
|
12825
|
-
|
|
12826
|
-
|
|
12827
|
-
await deleteSessionState(cwd, tool);
|
|
12828
|
-
}
|
|
12829
|
-
await initializeSession(cwd, tool, sessionId);
|
|
12830
|
-
} finally {
|
|
12831
|
-
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++;
|
|
12832
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
|
+
);
|
|
12833
14117
|
}
|
|
12834
|
-
|
|
12835
|
-
|
|
12836
|
-
|
|
12837
|
-
|
|
12838
|
-
|
|
12839
|
-
|
|
12840
|
-
|
|
12841
|
-
|
|
12842
|
-
|
|
12843
|
-
|
|
12844
|
-
|
|
12845
|
-
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
12852
|
-
|
|
12853
|
-
|
|
12854
|
-
|
|
12855
|
-
|
|
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})`
|
|
12856
14145
|
);
|
|
12857
|
-
|
|
12858
|
-
|
|
12859
|
-
|
|
12860
|
-
|
|
12861
|
-
|
|
12862
|
-
|
|
12863
|
-
|
|
12864
|
-
|
|
12865
|
-
|
|
12866
|
-
|
|
12867
|
-
|
|
12868
|
-
|
|
12869
|
-
|
|
12870
|
-
|
|
12871
|
-
|
|
12872
|
-
|
|
12873
|
-
|
|
12874
|
-
|
|
12875
|
-
|
|
12876
|
-
|
|
12877
|
-
|
|
12878
|
-
|
|
12879
|
-
|
|
12880
|
-
|
|
12881
|
-
|
|
12882
|
-
|
|
12883
|
-
|
|
12884
|
-
|
|
12885
|
-
|
|
12886
|
-
|
|
12887
|
-
|
|
12888
|
-
|
|
12889
|
-
|
|
12890
|
-
|
|
12891
|
-
|
|
12892
|
-
|
|
12893
|
-
|
|
12894
|
-
|
|
12895
|
-
|
|
12896
|
-
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) {
|
|
12897
14185
|
appendLog(
|
|
12898
14186
|
"info",
|
|
12899
|
-
`git-traces:
|
|
14187
|
+
`git-traces: skipping repo on Stop (repo=${repoRoot}, project=${config.projectId}, reason=no-active-state)`
|
|
12900
14188
|
);
|
|
14189
|
+
return "no-state";
|
|
12901
14190
|
}
|
|
12902
|
-
const
|
|
12903
|
-
|
|
12904
|
-
if (contributionId === null || lastSnapshotTreeSha === null) {
|
|
14191
|
+
const lastSnapshotTreeSha = state.lastSnapshotTreeSha ?? state.baselineTreeSha;
|
|
14192
|
+
if (!lastSnapshotTreeSha) {
|
|
12905
14193
|
appendLog(
|
|
12906
14194
|
"error",
|
|
12907
|
-
|
|
14195
|
+
`git-traces: invariant violation \u2014 session state missing snapshot tree SHA (repo=${repoRoot}, session=${state.sessionId})`
|
|
12908
14196
|
);
|
|
12909
|
-
return;
|
|
14197
|
+
return "failed";
|
|
12910
14198
|
}
|
|
12911
|
-
const currentHeadSha = captureHeadSha(
|
|
14199
|
+
const currentHeadSha = captureHeadSha(repoRoot);
|
|
12912
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
|
+
}
|
|
12913
14229
|
const transitionKind = detectTransitionKind(
|
|
12914
|
-
|
|
14230
|
+
repoRoot,
|
|
12915
14231
|
state.headSha || null,
|
|
12916
14232
|
currentHeadSha
|
|
12917
14233
|
);
|
|
12918
14234
|
const nextEpoch = state.epoch + 1;
|
|
12919
14235
|
appendLog(
|
|
12920
14236
|
"info",
|
|
12921
|
-
`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})`
|
|
12922
14238
|
);
|
|
12923
14239
|
const artifacts = await openEpoch({
|
|
12924
|
-
repoRoot
|
|
12925
|
-
client,
|
|
12926
|
-
contributionId,
|
|
14240
|
+
repoRoot,
|
|
14241
|
+
client: client2,
|
|
14242
|
+
contributionId: contributionId2,
|
|
12927
14243
|
sessionId: state.sessionId,
|
|
12928
14244
|
tool,
|
|
12929
14245
|
epoch: nextEpoch,
|
|
@@ -12931,7 +14247,7 @@ Uploaded: ${now.toISOString()}`
|
|
|
12931
14247
|
transitionKind,
|
|
12932
14248
|
startedAt: state.startedAt
|
|
12933
14249
|
});
|
|
12934
|
-
if (!artifacts) return;
|
|
14250
|
+
if (!artifacts) return "failed";
|
|
12935
14251
|
const next = {
|
|
12936
14252
|
...state,
|
|
12937
14253
|
baselineSha: artifacts.baselineSha,
|
|
@@ -12943,66 +14259,204 @@ Uploaded: ${now.toISOString()}`
|
|
|
12943
14259
|
headSha: artifacts.headSha
|
|
12944
14260
|
};
|
|
12945
14261
|
await writeSessionState(next, tool);
|
|
12946
|
-
|
|
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";
|
|
12947
14267
|
}
|
|
12948
|
-
const currentSha = captureSnapshotSha(
|
|
12949
|
-
const currentTreeSha = buildSnapshotTree(
|
|
14268
|
+
const currentSha = captureSnapshotSha(repoRoot);
|
|
14269
|
+
const currentTreeSha = buildSnapshotTree(repoRoot, currentSha);
|
|
12950
14270
|
const patchBuffer = createTreeDiffPatchGz(
|
|
12951
|
-
|
|
14271
|
+
repoRoot,
|
|
12952
14272
|
lastSnapshotTreeSha,
|
|
12953
14273
|
currentTreeSha
|
|
12954
14274
|
);
|
|
12955
14275
|
if (!patchBuffer) {
|
|
12956
14276
|
appendLog(
|
|
12957
14277
|
"info",
|
|
12958
|
-
|
|
14278
|
+
`git-traces: turn produced identical snapshot tree, skipping upload (repo=${repoRoot}, project=${config.projectId})`
|
|
12959
14279
|
);
|
|
12960
|
-
return;
|
|
14280
|
+
return "unchanged";
|
|
12961
14281
|
}
|
|
12962
|
-
state.turnCount++;
|
|
12963
14282
|
const prefix = epochPrefix(state.epoch);
|
|
12964
|
-
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
|
+
}
|
|
12965
14321
|
pinRef(
|
|
12966
|
-
|
|
14322
|
+
repoRoot,
|
|
12967
14323
|
`refs/hillclimb/turns/${state.sessionId}/${prefix}/${turnLabel}`,
|
|
12968
14324
|
currentSha
|
|
12969
14325
|
);
|
|
12970
|
-
const
|
|
12971
|
-
await uploadFile(
|
|
14326
|
+
const uploaded = await uploadFile(
|
|
12972
14327
|
client,
|
|
12973
14328
|
contributionId,
|
|
12974
14329
|
filename,
|
|
12975
14330
|
"application/gzip",
|
|
12976
14331
|
patchBuffer
|
|
12977
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;
|
|
12978
14341
|
state.lastSnapshotSha = currentSha;
|
|
12979
14342
|
state.lastSnapshotTreeSha = currentTreeSha;
|
|
12980
14343
|
await writeSessionState(state, tool);
|
|
12981
14344
|
appendLog(
|
|
12982
14345
|
"info",
|
|
12983
|
-
`git-traces:
|
|
14346
|
+
`git-traces: patch uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId}, epoch=${state.epoch}, turn=${turnLabel}, bytes=${patchBuffer.byteLength})`
|
|
12984
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";
|
|
12985
14355
|
} finally {
|
|
12986
|
-
await releaseLock(
|
|
14356
|
+
await releaseLock(repoRoot, tool);
|
|
12987
14357
|
}
|
|
12988
14358
|
}
|
|
12989
|
-
async function
|
|
14359
|
+
async function handleStop(payload, tool) {
|
|
12990
14360
|
const cwd = resolveCwd(payload);
|
|
12991
14361
|
if (!cwd) return;
|
|
12992
|
-
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);
|
|
12993
14410
|
try {
|
|
12994
|
-
const state = await readSessionState(
|
|
12995
|
-
if (!state) return;
|
|
12996
|
-
cleanupSessionRefs(
|
|
12997
|
-
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);
|
|
12998
14415
|
appendLog(
|
|
12999
14416
|
"info",
|
|
13000
|
-
`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)}`
|
|
13001
14424
|
);
|
|
14425
|
+
return "failed";
|
|
13002
14426
|
} finally {
|
|
13003
|
-
await releaseLock(
|
|
14427
|
+
await releaseLock(repoRoot, tool);
|
|
13004
14428
|
}
|
|
13005
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
|
+
}
|
|
13006
14460
|
|
|
13007
14461
|
// src/git-traces/index.ts
|
|
13008
14462
|
var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
|
|
@@ -13018,6 +14472,25 @@ var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
|
13018
14472
|
"cursor",
|
|
13019
14473
|
"opencode"
|
|
13020
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
|
+
}
|
|
13021
14494
|
function parseToolArg2(argv) {
|
|
13022
14495
|
for (let i = 0; i < argv.length; i++) {
|
|
13023
14496
|
const a = argv[i];
|
|
@@ -13034,6 +14507,96 @@ async function readStdin2() {
|
|
|
13034
14507
|
}
|
|
13035
14508
|
return Buffer.concat(chunks).toString("utf-8");
|
|
13036
14509
|
}
|
|
14510
|
+
function resolveCwd2(payload) {
|
|
14511
|
+
return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
|
|
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
|
+
}
|
|
14523
|
+
async function selfHealHook2(payload, tool) {
|
|
14524
|
+
if (process.env.HILLCLIMB_SKIP_HOOK_SELF_HEAL === "1") return;
|
|
14525
|
+
const cwd = resolveCwd2(payload);
|
|
14526
|
+
if (!cwd) return;
|
|
14527
|
+
try {
|
|
14528
|
+
const project = await findProjectForCwd(cwd);
|
|
14529
|
+
if (!project) return;
|
|
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;
|
|
14565
|
+
}
|
|
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);
|
|
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];
|
|
14592
|
+
} catch (err) {
|
|
14593
|
+
appendLog(
|
|
14594
|
+
"warn",
|
|
14595
|
+
`git-traces: failed legacy hook repair: ${formatError(err)}`
|
|
14596
|
+
);
|
|
14597
|
+
return null;
|
|
14598
|
+
}
|
|
14599
|
+
}
|
|
13037
14600
|
async function runGitTraces() {
|
|
13038
14601
|
if (process.env[WORKER_ENV_FLAG2] === "1") {
|
|
13039
14602
|
await runGitTracesWorker();
|
|
@@ -13046,13 +14609,6 @@ async function runGitTraces() {
|
|
|
13046
14609
|
"info",
|
|
13047
14610
|
`git-traces hook invoked (pid ${process.pid}, tool=${toolArg ?? "<none>"})`
|
|
13048
14611
|
);
|
|
13049
|
-
if (!toolArg || !KNOWN_TOOLS.has(toolArg)) {
|
|
13050
|
-
appendLog(
|
|
13051
|
-
"error",
|
|
13052
|
-
`git-traces: missing or unknown --tool arg (got ${toolArg ?? "<none>"})`
|
|
13053
|
-
);
|
|
13054
|
-
return;
|
|
13055
|
-
}
|
|
13056
14612
|
let raw;
|
|
13057
14613
|
try {
|
|
13058
14614
|
raw = await readStdin2();
|
|
@@ -13064,6 +14620,15 @@ async function runGitTraces() {
|
|
|
13064
14620
|
appendLog("warn", "git-traces: empty stdin, expected hook payload");
|
|
13065
14621
|
return;
|
|
13066
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
|
+
}
|
|
13067
14632
|
const entrypoint = process.argv[1];
|
|
13068
14633
|
if (!entrypoint) {
|
|
13069
14634
|
appendLog("error", "git-traces: process.argv[1] is empty");
|
|
@@ -13072,14 +14637,14 @@ async function runGitTraces() {
|
|
|
13072
14637
|
try {
|
|
13073
14638
|
const child = spawn3(
|
|
13074
14639
|
process.execPath,
|
|
13075
|
-
[entrypoint, "git-traces", `--tool=${
|
|
14640
|
+
[entrypoint, "git-traces", `--tool=${tool}`],
|
|
13076
14641
|
{
|
|
13077
14642
|
detached: true,
|
|
13078
14643
|
stdio: ["pipe", "ignore", "ignore"],
|
|
13079
14644
|
env: {
|
|
13080
14645
|
...process.env,
|
|
13081
14646
|
[WORKER_ENV_FLAG2]: "1",
|
|
13082
|
-
[TOOL_ENV_FLAG2]:
|
|
14647
|
+
[TOOL_ENV_FLAG2]: tool,
|
|
13083
14648
|
[FLOW_ID_ENV]: flowId
|
|
13084
14649
|
}
|
|
13085
14650
|
}
|
|
@@ -13148,26 +14713,16 @@ async function runGitTracesWorker() {
|
|
|
13148
14713
|
);
|
|
13149
14714
|
return;
|
|
13150
14715
|
}
|
|
14716
|
+
await selfHealHook2(payload, tool);
|
|
13151
14717
|
try {
|
|
13152
|
-
switch (event) {
|
|
13153
|
-
case "SessionStart":
|
|
14718
|
+
switch (classifyHookEvent(event)) {
|
|
13154
14719
|
case "sessionStart":
|
|
13155
|
-
// opencode: session.created fires when a new session is first opened.
|
|
13156
|
-
case "session.created":
|
|
13157
14720
|
await handleSessionStart(payload, tool);
|
|
13158
14721
|
break;
|
|
13159
|
-
case "Stop":
|
|
13160
14722
|
case "stop":
|
|
13161
|
-
// opencode: session.idle fires at end-of-turn (agent finished responding).
|
|
13162
|
-
case "session.idle":
|
|
13163
14723
|
await handleStop(payload, tool);
|
|
13164
14724
|
break;
|
|
13165
|
-
case "SessionEnd":
|
|
13166
14725
|
case "sessionEnd":
|
|
13167
|
-
// opencode: server.instance.disposed fires when opencode shuts down.
|
|
13168
|
-
// Payload has `cwd` (from the plugin) but no session_id; handleSessionEnd
|
|
13169
|
-
// reads state by (repoRoot, tool), so cwd alone is sufficient for cleanup.
|
|
13170
|
-
case "server.instance.disposed":
|
|
13171
14726
|
await handleSessionEnd(payload, tool);
|
|
13172
14727
|
break;
|
|
13173
14728
|
default:
|
|
@@ -13186,14 +14741,14 @@ ${stack}` : ""}`
|
|
|
13186
14741
|
|
|
13187
14742
|
// src/outputs/zip.ts
|
|
13188
14743
|
import fs13 from "fs";
|
|
13189
|
-
import
|
|
14744
|
+
import path17 from "path";
|
|
13190
14745
|
import archiver2 from "archiver";
|
|
13191
14746
|
|
|
13192
14747
|
// src/outputs/downloads.ts
|
|
13193
14748
|
import { execSync as execSync2 } from "child_process";
|
|
13194
14749
|
import fs12 from "fs";
|
|
13195
14750
|
import os8 from "os";
|
|
13196
|
-
import
|
|
14751
|
+
import path16 from "path";
|
|
13197
14752
|
function getDownloadsFolder() {
|
|
13198
14753
|
const home = os8.homedir();
|
|
13199
14754
|
if (process.platform === "linux") {
|
|
@@ -13206,7 +14761,7 @@ function getDownloadsFolder() {
|
|
|
13206
14761
|
} catch {
|
|
13207
14762
|
}
|
|
13208
14763
|
}
|
|
13209
|
-
const downloads =
|
|
14764
|
+
const downloads = path16.join(home, "Downloads");
|
|
13210
14765
|
if (fs12.existsSync(downloads)) return downloads;
|
|
13211
14766
|
return home;
|
|
13212
14767
|
}
|
|
@@ -13216,11 +14771,11 @@ function sanitizeFilename(name) {
|
|
|
13216
14771
|
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
13217
14772
|
}
|
|
13218
14773
|
function getUniqueFilename(dir, base, ext) {
|
|
13219
|
-
let candidate =
|
|
14774
|
+
let candidate = path17.join(dir, `${base}${ext}`);
|
|
13220
14775
|
if (!fs13.existsSync(candidate)) return candidate;
|
|
13221
14776
|
let i = 1;
|
|
13222
14777
|
while (fs13.existsSync(candidate)) {
|
|
13223
|
-
candidate =
|
|
14778
|
+
candidate = path17.join(dir, `${base}-${i}${ext}`);
|
|
13224
14779
|
i++;
|
|
13225
14780
|
}
|
|
13226
14781
|
return candidate;
|
|
@@ -13230,7 +14785,7 @@ var ZipOutput = class {
|
|
|
13230
14785
|
label = "Save as .zip to Downloads";
|
|
13231
14786
|
async emit(group, options) {
|
|
13232
14787
|
const downloadsDir = getDownloadsFolder();
|
|
13233
|
-
const repoName = sanitizeFilename(
|
|
14788
|
+
const repoName = sanitizeFilename(path17.basename(group.repoPath));
|
|
13234
14789
|
const timeRange = options.timeRange;
|
|
13235
14790
|
const rangePart = timeRange?.label ?? "all";
|
|
13236
14791
|
const epochSeconds = Math.floor(Date.now() / 1e3);
|
|
@@ -13432,11 +14987,11 @@ async function confirmExport(group, output) {
|
|
|
13432
14987
|
// src/sources/claude.ts
|
|
13433
14988
|
import fs14 from "fs";
|
|
13434
14989
|
import os9 from "os";
|
|
13435
|
-
import
|
|
14990
|
+
import path18 from "path";
|
|
13436
14991
|
import readline from "readline";
|
|
13437
14992
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
|
|
13438
14993
|
async function resolveRepoPath(projectDir) {
|
|
13439
|
-
const indexPath =
|
|
14994
|
+
const indexPath = path18.join(projectDir, "sessions-index.json");
|
|
13440
14995
|
try {
|
|
13441
14996
|
const raw = await fs14.promises.readFile(indexPath, "utf-8");
|
|
13442
14997
|
const data = JSON.parse(raw);
|
|
@@ -13451,7 +15006,7 @@ async function resolveRepoPath(projectDir) {
|
|
|
13451
15006
|
});
|
|
13452
15007
|
for (const entry of entries) {
|
|
13453
15008
|
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
13454
|
-
const cwd = await extractCwdFromJsonl(
|
|
15009
|
+
const cwd = await extractCwdFromJsonl(path18.join(projectDir, entry.name));
|
|
13455
15010
|
if (cwd) {
|
|
13456
15011
|
cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
|
|
13457
15012
|
}
|
|
@@ -13497,7 +15052,7 @@ async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
|
|
|
13497
15052
|
return;
|
|
13498
15053
|
}
|
|
13499
15054
|
for (const entry of entries) {
|
|
13500
|
-
const fullPath =
|
|
15055
|
+
const fullPath = path18.join(dir, entry.name);
|
|
13501
15056
|
if (entry.isDirectory()) {
|
|
13502
15057
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
13503
15058
|
await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
|
|
@@ -13519,7 +15074,7 @@ function fallbackDecode(encodedName) {
|
|
|
13519
15074
|
var ClaudeSource = class {
|
|
13520
15075
|
name = "claude";
|
|
13521
15076
|
async scan() {
|
|
13522
|
-
const baseDir =
|
|
15077
|
+
const baseDir = path18.join(os9.homedir(), ".claude", "projects");
|
|
13523
15078
|
try {
|
|
13524
15079
|
await fs14.promises.access(baseDir);
|
|
13525
15080
|
} catch {
|
|
@@ -13531,7 +15086,7 @@ var ClaudeSource = class {
|
|
|
13531
15086
|
const dirEntries = projectDirs.filter((d) => d.isDirectory());
|
|
13532
15087
|
const resultArrays = await Promise.all(
|
|
13533
15088
|
dirEntries.map(async (dir) => {
|
|
13534
|
-
const projectPath =
|
|
15089
|
+
const projectPath = path18.join(baseDir, dir.name);
|
|
13535
15090
|
const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
|
|
13536
15091
|
const files = [];
|
|
13537
15092
|
await collectFiles(
|
|
@@ -13551,7 +15106,7 @@ var ClaudeSource = class {
|
|
|
13551
15106
|
// src/sources/codex.ts
|
|
13552
15107
|
import fs15 from "fs";
|
|
13553
15108
|
import os10 from "os";
|
|
13554
|
-
import
|
|
15109
|
+
import path19 from "path";
|
|
13555
15110
|
import readline2 from "readline";
|
|
13556
15111
|
async function parseSessionMeta(filePath) {
|
|
13557
15112
|
const stream = fs15.createReadStream(filePath, { encoding: "utf-8" });
|
|
@@ -13584,7 +15139,7 @@ async function findJsonlFiles(dir) {
|
|
|
13584
15139
|
return;
|
|
13585
15140
|
}
|
|
13586
15141
|
for (const entry of entries) {
|
|
13587
|
-
const full =
|
|
15142
|
+
const full = path19.join(d, entry.name);
|
|
13588
15143
|
if (entry.isDirectory()) {
|
|
13589
15144
|
await walk(full);
|
|
13590
15145
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -13629,14 +15184,14 @@ async function loadHistory(historyPath) {
|
|
|
13629
15184
|
var CodexSource = class {
|
|
13630
15185
|
name = "codex";
|
|
13631
15186
|
async scan() {
|
|
13632
|
-
const codexDir =
|
|
13633
|
-
const sessionsDir =
|
|
15187
|
+
const codexDir = path19.join(os10.homedir(), ".codex");
|
|
15188
|
+
const sessionsDir = path19.join(codexDir, "sessions");
|
|
13634
15189
|
try {
|
|
13635
15190
|
await fs15.promises.access(sessionsDir);
|
|
13636
15191
|
} catch {
|
|
13637
15192
|
return [];
|
|
13638
15193
|
}
|
|
13639
|
-
const historyPath =
|
|
15194
|
+
const historyPath = path19.join(codexDir, "history.jsonl");
|
|
13640
15195
|
const [jsonlFiles, historyMap] = await Promise.all([
|
|
13641
15196
|
findJsonlFiles(sessionsDir),
|
|
13642
15197
|
loadHistory(historyPath)
|
|
@@ -13659,8 +15214,8 @@ var CodexSource = class {
|
|
|
13659
15214
|
});
|
|
13660
15215
|
const historyLines = historyMap.get(meta.sessionId);
|
|
13661
15216
|
if (historyLines) {
|
|
13662
|
-
const sessionDir =
|
|
13663
|
-
const historyAbsPath =
|
|
15217
|
+
const sessionDir = path19.relative(sessionsDir, path19.dirname(filePath));
|
|
15218
|
+
const historyAbsPath = path19.join(
|
|
13664
15219
|
sessionsDir,
|
|
13665
15220
|
sessionDir,
|
|
13666
15221
|
`history-${meta.sessionId}.jsonl`
|
|
@@ -13682,16 +15237,16 @@ var CodexSource = class {
|
|
|
13682
15237
|
// src/sources/copilotChat.ts
|
|
13683
15238
|
import fs16 from "fs";
|
|
13684
15239
|
import os11 from "os";
|
|
13685
|
-
import
|
|
15240
|
+
import path20 from "path";
|
|
13686
15241
|
import { fileURLToPath } from "url";
|
|
13687
15242
|
function vsCodeUserDirs() {
|
|
13688
15243
|
const home = os11.homedir();
|
|
13689
15244
|
const dirs = [
|
|
13690
|
-
|
|
13691
|
-
|
|
15245
|
+
path20.join(home, "Library", "Application Support", "Code", "User"),
|
|
15246
|
+
path20.join(home, ".config", "Code", "User")
|
|
13692
15247
|
];
|
|
13693
15248
|
if (process.env.APPDATA) {
|
|
13694
|
-
dirs.push(
|
|
15249
|
+
dirs.push(path20.join(process.env.APPDATA, "Code", "User"));
|
|
13695
15250
|
}
|
|
13696
15251
|
return dirs;
|
|
13697
15252
|
}
|
|
@@ -13728,7 +15283,7 @@ var CopilotChatSource = class {
|
|
|
13728
15283
|
async scan() {
|
|
13729
15284
|
const results = [];
|
|
13730
15285
|
for (const userDir of vsCodeUserDirs()) {
|
|
13731
|
-
const workspaceStorage =
|
|
15286
|
+
const workspaceStorage = path20.join(userDir, "workspaceStorage");
|
|
13732
15287
|
let hashDirs;
|
|
13733
15288
|
try {
|
|
13734
15289
|
hashDirs = await fs16.promises.readdir(workspaceStorage, {
|
|
@@ -13739,8 +15294,8 @@ var CopilotChatSource = class {
|
|
|
13739
15294
|
}
|
|
13740
15295
|
for (const hash of hashDirs) {
|
|
13741
15296
|
if (!hash.isDirectory()) continue;
|
|
13742
|
-
const wsRoot =
|
|
13743
|
-
const transcriptsDir =
|
|
15297
|
+
const wsRoot = path20.join(workspaceStorage, hash.name);
|
|
15298
|
+
const transcriptsDir = path20.join(
|
|
13744
15299
|
wsRoot,
|
|
13745
15300
|
"GitHub.copilot-chat",
|
|
13746
15301
|
"transcripts"
|
|
@@ -13754,7 +15309,7 @@ var CopilotChatSource = class {
|
|
|
13754
15309
|
continue;
|
|
13755
15310
|
}
|
|
13756
15311
|
const repoPath = await readWorkspaceFolder(
|
|
13757
|
-
|
|
15312
|
+
path20.join(wsRoot, "workspace.json")
|
|
13758
15313
|
);
|
|
13759
15314
|
if (!repoPath) continue;
|
|
13760
15315
|
for (const entry of transcriptEntries) {
|
|
@@ -13762,7 +15317,7 @@ var CopilotChatSource = class {
|
|
|
13762
15317
|
const sessionId = entry.name.slice(0, -".jsonl".length);
|
|
13763
15318
|
results.push({
|
|
13764
15319
|
sourceName: this.name,
|
|
13765
|
-
absolutePath:
|
|
15320
|
+
absolutePath: path20.join(transcriptsDir, entry.name),
|
|
13766
15321
|
repoPath,
|
|
13767
15322
|
metadata: { sessionId }
|
|
13768
15323
|
});
|
|
@@ -13829,10 +15384,10 @@ async function runInteractive() {
|
|
|
13829
15384
|
s.start(`Scanning ${source.name} logs...`);
|
|
13830
15385
|
const allFiles = await source.scan();
|
|
13831
15386
|
const allGroups = await mergeByRepo(allFiles);
|
|
13832
|
-
const repoRoot =
|
|
15387
|
+
const repoRoot = path21.resolve(repo.root);
|
|
13833
15388
|
const matching = allGroups.filter((g) => {
|
|
13834
|
-
const resolved =
|
|
13835
|
-
return resolved === repoRoot || resolved.startsWith(repoRoot +
|
|
15389
|
+
const resolved = path21.resolve(g.repoPath);
|
|
15390
|
+
return resolved === repoRoot || resolved.startsWith(repoRoot + path21.sep);
|
|
13836
15391
|
});
|
|
13837
15392
|
if (matching.length === 0) {
|
|
13838
15393
|
s.stop(`No ${source.name} logs found for ${repo.name}.`);
|
|
@@ -13863,7 +15418,7 @@ async function runInteractive() {
|
|
|
13863
15418
|
}
|
|
13864
15419
|
}
|
|
13865
15420
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
13866
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
15421
|
+
const envFilePaths = envFileNames.map((n) => path21.join(repoRoot, n));
|
|
13867
15422
|
const additionalFiles = await promptSecretFiles(envFileNames);
|
|
13868
15423
|
const secretResult = await collectSecrets(
|
|
13869
15424
|
repoRoot,
|