hillclimb 0.4.4 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +907 -394
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs19 from "fs";
|
|
5
|
+
import path24 from "path";
|
|
6
6
|
import * as p6 from "@clack/prompts";
|
|
7
7
|
|
|
8
8
|
// src/commands/init.ts
|
|
9
|
+
import path7 from "path";
|
|
9
10
|
import * as p3 from "@clack/prompts";
|
|
10
11
|
|
|
11
12
|
// src/color.ts
|
|
@@ -124,67 +125,18 @@ function detectRepoRoot() {
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
// src/
|
|
128
|
+
// src/identity.ts
|
|
128
129
|
import fs2 from "fs";
|
|
129
130
|
import path3 from "path";
|
|
130
|
-
function toGitignorePattern(repoRoot, file) {
|
|
131
|
-
const rel = path3.relative(repoRoot, file);
|
|
132
|
-
if (!rel || rel.startsWith("..") || path3.isAbsolute(rel)) return null;
|
|
133
|
-
return `/${rel.split(path3.sep).join("/")}`;
|
|
134
|
-
}
|
|
135
|
-
function normalizePattern(pattern) {
|
|
136
|
-
return pattern.trim().replace(/\\/g, "/").replace(/^\/+/, "");
|
|
137
|
-
}
|
|
138
|
-
async function ensureGitignored(repoRoot, files) {
|
|
139
|
-
const gitignorePath = path3.join(repoRoot, ".gitignore");
|
|
140
|
-
const patterns = [
|
|
141
|
-
...new Set(
|
|
142
|
-
files.map((file) => toGitignorePattern(repoRoot, file)).filter((pattern) => pattern !== null)
|
|
143
|
-
)
|
|
144
|
-
];
|
|
145
|
-
let content = "";
|
|
146
|
-
let created = false;
|
|
147
|
-
try {
|
|
148
|
-
content = await fs2.promises.readFile(gitignorePath, "utf-8");
|
|
149
|
-
} catch (err) {
|
|
150
|
-
if (err.code !== "ENOENT") throw err;
|
|
151
|
-
created = true;
|
|
152
|
-
}
|
|
153
|
-
const existing = new Set(
|
|
154
|
-
content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map(normalizePattern)
|
|
155
|
-
);
|
|
156
|
-
const added = patterns.filter(
|
|
157
|
-
(pattern) => !existing.has(normalizePattern(pattern))
|
|
158
|
-
);
|
|
159
|
-
if (added.length === 0) {
|
|
160
|
-
return { path: gitignorePath, added: [], created: false, changed: false };
|
|
161
|
-
}
|
|
162
|
-
let next = content;
|
|
163
|
-
if (next && !next.endsWith("\n")) next += "\n";
|
|
164
|
-
if (next) next += "\n";
|
|
165
|
-
next += ["# Hillclimb hook files", ...added].join("\n");
|
|
166
|
-
next += "\n";
|
|
167
|
-
await fs2.promises.writeFile(gitignorePath, next);
|
|
168
|
-
return {
|
|
169
|
-
path: gitignorePath,
|
|
170
|
-
added,
|
|
171
|
-
created,
|
|
172
|
-
changed: true
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// src/identity.ts
|
|
177
|
-
import fs3 from "fs";
|
|
178
|
-
import path4 from "path";
|
|
179
131
|
function identityPath() {
|
|
180
|
-
return
|
|
132
|
+
return path3.join(configDir(), "identity.json");
|
|
181
133
|
}
|
|
182
134
|
function normalizeUrl(apiBaseUrl) {
|
|
183
135
|
return apiBaseUrl.replace(/\/$/, "");
|
|
184
136
|
}
|
|
185
137
|
async function loadAllIdentities() {
|
|
186
138
|
try {
|
|
187
|
-
const raw = await
|
|
139
|
+
const raw = await fs2.promises.readFile(identityPath(), "utf-8");
|
|
188
140
|
const parsed = JSON.parse(raw);
|
|
189
141
|
if (!parsed.identities || typeof parsed.identities !== "object") {
|
|
190
142
|
return { identities: {} };
|
|
@@ -199,12 +151,12 @@ async function loadIdentity(apiBaseUrl) {
|
|
|
199
151
|
return file.identities[normalizeUrl(apiBaseUrl)] ?? null;
|
|
200
152
|
}
|
|
201
153
|
async function writeIdentityFile(file) {
|
|
202
|
-
await
|
|
154
|
+
await fs2.promises.mkdir(configDir(), { recursive: true, mode: 448 });
|
|
203
155
|
const tmp = `${identityPath()}.tmp`;
|
|
204
|
-
await
|
|
156
|
+
await fs2.promises.writeFile(tmp, JSON.stringify(file, null, 2), {
|
|
205
157
|
mode: 384
|
|
206
158
|
});
|
|
207
|
-
await
|
|
159
|
+
await fs2.promises.rename(tmp, identityPath());
|
|
208
160
|
}
|
|
209
161
|
async function saveIdentity(identity) {
|
|
210
162
|
const file = await loadAllIdentities();
|
|
@@ -242,8 +194,8 @@ function formatError(err) {
|
|
|
242
194
|
}
|
|
243
195
|
|
|
244
196
|
// src/platform/log.ts
|
|
245
|
-
import
|
|
246
|
-
import
|
|
197
|
+
import fs3 from "fs";
|
|
198
|
+
import path4 from "path";
|
|
247
199
|
var PT_TIME_ZONE = "America/Los_Angeles";
|
|
248
200
|
function pacificParts(date) {
|
|
249
201
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
@@ -294,10 +246,10 @@ function pacificDateString(date) {
|
|
|
294
246
|
return `${p7.year}-${p7.month}-${p7.day}`;
|
|
295
247
|
}
|
|
296
248
|
function logsDir() {
|
|
297
|
-
return
|
|
249
|
+
return path4.join(configDir(), "logs");
|
|
298
250
|
}
|
|
299
251
|
function todayLogPath() {
|
|
300
|
-
return
|
|
252
|
+
return path4.join(logsDir(), `${pacificDateString(/* @__PURE__ */ new Date())}.log`);
|
|
301
253
|
}
|
|
302
254
|
var logPrefix = "";
|
|
303
255
|
function setLogPrefix(prefix) {
|
|
@@ -308,8 +260,8 @@ function appendLog(level, message) {
|
|
|
308
260
|
const line = `[${pacificTimestamp(/* @__PURE__ */ new Date())}] [${level}]${tag} ${message}
|
|
309
261
|
`;
|
|
310
262
|
try {
|
|
311
|
-
|
|
312
|
-
|
|
263
|
+
fs3.mkdirSync(logsDir(), { recursive: true, mode: 448 });
|
|
264
|
+
fs3.appendFileSync(todayLogPath(), line);
|
|
313
265
|
} catch {
|
|
314
266
|
process.stderr.write(`hillclimb:${tag} ${level}: ${message}
|
|
315
267
|
`);
|
|
@@ -597,9 +549,61 @@ var PlatformClient = class {
|
|
|
597
549
|
};
|
|
598
550
|
|
|
599
551
|
// src/platform/hooks.ts
|
|
552
|
+
import { execFileSync } from "child_process";
|
|
600
553
|
import fs5 from "fs";
|
|
601
554
|
import os2 from "os";
|
|
602
555
|
import path6 from "path";
|
|
556
|
+
|
|
557
|
+
// src/gitignore.ts
|
|
558
|
+
import fs4 from "fs";
|
|
559
|
+
import path5 from "path";
|
|
560
|
+
function toGitignorePattern(repoRoot, file) {
|
|
561
|
+
const rel = path5.relative(repoRoot, file);
|
|
562
|
+
if (!rel || rel.startsWith("..") || path5.isAbsolute(rel)) return null;
|
|
563
|
+
return `/${rel.split(path5.sep).join("/")}`;
|
|
564
|
+
}
|
|
565
|
+
function normalizePattern(pattern) {
|
|
566
|
+
return pattern.trim().replace(/\\/g, "/").replace(/^\/+/, "");
|
|
567
|
+
}
|
|
568
|
+
async function ensureGitignored(repoRoot, files) {
|
|
569
|
+
const gitignorePath = path5.join(repoRoot, ".gitignore");
|
|
570
|
+
const patterns = [
|
|
571
|
+
...new Set(
|
|
572
|
+
files.map((file) => toGitignorePattern(repoRoot, file)).filter((pattern) => pattern !== null)
|
|
573
|
+
)
|
|
574
|
+
];
|
|
575
|
+
let content = "";
|
|
576
|
+
let created = false;
|
|
577
|
+
try {
|
|
578
|
+
content = await fs4.promises.readFile(gitignorePath, "utf-8");
|
|
579
|
+
} catch (err) {
|
|
580
|
+
if (err.code !== "ENOENT") throw err;
|
|
581
|
+
created = true;
|
|
582
|
+
}
|
|
583
|
+
const existing = new Set(
|
|
584
|
+
content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map(normalizePattern)
|
|
585
|
+
);
|
|
586
|
+
const added = patterns.filter(
|
|
587
|
+
(pattern) => !existing.has(normalizePattern(pattern))
|
|
588
|
+
);
|
|
589
|
+
if (added.length === 0) {
|
|
590
|
+
return { path: gitignorePath, added: [], created: false, changed: false };
|
|
591
|
+
}
|
|
592
|
+
let next = content;
|
|
593
|
+
if (next && !next.endsWith("\n")) next += "\n";
|
|
594
|
+
if (next) next += "\n";
|
|
595
|
+
next += ["# Hillclimb hook files", ...added].join("\n");
|
|
596
|
+
next += "\n";
|
|
597
|
+
await fs4.promises.writeFile(gitignorePath, next);
|
|
598
|
+
return {
|
|
599
|
+
path: gitignorePath,
|
|
600
|
+
added,
|
|
601
|
+
created,
|
|
602
|
+
changed: true
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/platform/hooks.ts
|
|
603
607
|
var HOOK_CMD = (sub) => `npx hillclimb@latest ${sub}`;
|
|
604
608
|
var GIT_TRACES_CMD = (tool) => `npx hillclimb@latest git-traces --tool=${tool}`;
|
|
605
609
|
var CLAUDE_SESSIONEND_UPLOAD_TIMEOUT_SECONDS = 30;
|
|
@@ -617,6 +621,15 @@ var TOOLS = [
|
|
|
617
621
|
command: HOOK_CMD("upload"),
|
|
618
622
|
timeout: CLAUDE_SESSIONEND_UPLOAD_TIMEOUT_SECONDS
|
|
619
623
|
},
|
|
624
|
+
// Per-turn transcript capture. SessionEnd is the clean one-shot
|
|
625
|
+
// upload, but it never fires for sessions that are killed or never
|
|
626
|
+
// cleanly closed (e.g. long-lived remote sessions), so those
|
|
627
|
+
// transcripts are lost entirely. Uploading on Stop captures them
|
|
628
|
+
// turn-by-turn. `--tool=claude` is required: resolveSourceTool()
|
|
629
|
+
// defaults a bare `upload` on a Stop event to Codex, and the explicit
|
|
630
|
+
// tool also keys the debug-log agent/git completion correlation to
|
|
631
|
+
// "claude" (see expectedKinds() in debug-logs.ts).
|
|
632
|
+
{ eventName: "Stop", command: `${HOOK_CMD("upload")} --tool=claude` },
|
|
620
633
|
{ eventName: "SessionStart", command: GIT_TRACES_CMD("claude") },
|
|
621
634
|
{ eventName: "Stop", command: GIT_TRACES_CMD("claude") },
|
|
622
635
|
{ eventName: "SessionEnd", command: GIT_TRACES_CMD("claude") }
|
|
@@ -630,6 +643,10 @@ var TOOLS = [
|
|
|
630
643
|
format: "cursor",
|
|
631
644
|
events: [
|
|
632
645
|
{ eventName: "sessionEnd", command: HOOK_CMD("upload") },
|
|
646
|
+
// Per-turn capture, mirroring Claude. Cursor's stop payload carries a
|
|
647
|
+
// populated transcript_path (verified on Cursor 3.0.16); --tool=cursor
|
|
648
|
+
// is required so a bare Stop upload isn't misattributed to Codex.
|
|
649
|
+
{ eventName: "stop", command: `${HOOK_CMD("upload")} --tool=cursor` },
|
|
633
650
|
{ eventName: "sessionStart", command: GIT_TRACES_CMD("cursor") },
|
|
634
651
|
{ eventName: "stop", command: GIT_TRACES_CMD("cursor") },
|
|
635
652
|
{ eventName: "sessionEnd", command: GIT_TRACES_CMD("cursor") }
|
|
@@ -710,20 +727,41 @@ function copilotChatDetect() {
|
|
|
710
727
|
return candidates.some(isDir);
|
|
711
728
|
}
|
|
712
729
|
function ensureHooks(settings) {
|
|
713
|
-
if (!settings.hooks
|
|
730
|
+
if (!settings.hooks || typeof settings.hooks !== "object" || Array.isArray(settings.hooks)) {
|
|
731
|
+
settings.hooks = {};
|
|
732
|
+
}
|
|
714
733
|
return settings.hooks;
|
|
715
734
|
}
|
|
716
735
|
function settingsPath(repoRoot, def) {
|
|
717
736
|
return path6.join(repoRoot, def.settingsFile);
|
|
718
737
|
}
|
|
719
|
-
async function readJson(file) {
|
|
738
|
+
async function readJson(file, options = {}) {
|
|
720
739
|
try {
|
|
721
740
|
const raw = await fs5.promises.readFile(file, "utf-8");
|
|
741
|
+
if (!raw.trim()) return {};
|
|
722
742
|
const parsed = JSON.parse(raw);
|
|
723
743
|
if (parsed && typeof parsed === "object") return parsed;
|
|
724
744
|
return {};
|
|
725
745
|
} catch (err) {
|
|
726
746
|
if (err.code === "ENOENT") return {};
|
|
747
|
+
if (options.repairMalformed && err instanceof SyntaxError) {
|
|
748
|
+
const backup = `${file}.malformed-${Date.now()}`;
|
|
749
|
+
try {
|
|
750
|
+
const raw = await fs5.promises.readFile(file, "utf-8");
|
|
751
|
+
await fs5.promises.writeFile(backup, raw);
|
|
752
|
+
appendLog(
|
|
753
|
+
"warn",
|
|
754
|
+
`hooks: backed up malformed JSON hook file ${file} to ${backup}`
|
|
755
|
+
);
|
|
756
|
+
} catch (backupErr) {
|
|
757
|
+
appendLog(
|
|
758
|
+
"warn",
|
|
759
|
+
`hooks: could not back up malformed JSON hook file ${file}: ${backupErr instanceof Error ? backupErr.message : String(backupErr)}`
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
return {};
|
|
763
|
+
}
|
|
764
|
+
if (err instanceof SyntaxError) return {};
|
|
727
765
|
throw err;
|
|
728
766
|
}
|
|
729
767
|
}
|
|
@@ -854,7 +892,7 @@ function copilotUninstallMatching(settings, eventName, predicate) {
|
|
|
854
892
|
}
|
|
855
893
|
return true;
|
|
856
894
|
}
|
|
857
|
-
var OPENCODE_PLUGIN_VERSION =
|
|
895
|
+
var OPENCODE_PLUGIN_VERSION = 7;
|
|
858
896
|
var OPENCODE_PLUGIN_MARKER = `// HILLCLIMB_OPENCODE_PLUGIN_VERSION=${OPENCODE_PLUGIN_VERSION}`;
|
|
859
897
|
var OPENCODE_PLUGIN_CONTENT = `${OPENCODE_PLUGIN_MARKER}
|
|
860
898
|
// Auto-installed by \`npx hillclimb\`. Do not edit manually \u2014 re-running
|
|
@@ -983,12 +1021,29 @@ export const HillclimbPlugin = async ({ directory }) => ({
|
|
|
983
1021
|
}
|
|
984
1022
|
|
|
985
1023
|
if (type === "session.idle" && sessionID) {
|
|
986
|
-
// Per-turn
|
|
987
|
-
//
|
|
988
|
-
//
|
|
1024
|
+
// Per-turn snapshot: upload the transcript-so-far AND take a git-traces
|
|
1025
|
+
// snapshot. The CLI keeps this to one contribution per session (reuse),
|
|
1026
|
+
// attaching the latest transcript on each turn, so this is one
|
|
1027
|
+
// contribution per session, not per turn. Keep the buffer \u2014 it's dropped
|
|
1028
|
+
// only on session.deleted / server.instance.disposed.
|
|
1029
|
+
//
|
|
1030
|
+
// Pass transcript_path to BOTH spawns: the debug-log eventId for a stop
|
|
1031
|
+
// event folds in transcript_path, so diverging payloads would orphan the
|
|
1032
|
+
// two completions and double-upload the debug log.
|
|
1033
|
+
const transcriptPath = writeTranscript(sessionID);
|
|
1034
|
+
if (transcriptPath) {
|
|
1035
|
+
spawnHillclimb("upload", {
|
|
1036
|
+
session_id: sessionID,
|
|
1037
|
+
cwd,
|
|
1038
|
+
transcript_path: transcriptPath,
|
|
1039
|
+
hook_event_name: "session.idle",
|
|
1040
|
+
tool: TOOL,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
989
1043
|
spawnHillclimb("git-traces --tool=opencode", {
|
|
990
1044
|
session_id: sessionID,
|
|
991
1045
|
cwd,
|
|
1046
|
+
transcript_path: transcriptPath || undefined,
|
|
992
1047
|
hook_event_name: "session.idle",
|
|
993
1048
|
tool: TOOL,
|
|
994
1049
|
});
|
|
@@ -1137,6 +1192,19 @@ function legacyBareGitTracesCommandsFor(command) {
|
|
|
1137
1192
|
(legacy) => legacy.endsWith(" git-traces")
|
|
1138
1193
|
);
|
|
1139
1194
|
}
|
|
1195
|
+
function legacyBareUploadCommandsFor(command) {
|
|
1196
|
+
const prefix = "npx hillclimb@latest ";
|
|
1197
|
+
if (!command.startsWith(prefix)) return [];
|
|
1198
|
+
const sub = command.slice(prefix.length);
|
|
1199
|
+
if (!sub.startsWith("upload --tool=")) return [];
|
|
1200
|
+
return [
|
|
1201
|
+
"npx hillclimb@latest upload",
|
|
1202
|
+
"npx hillclimb upload",
|
|
1203
|
+
"hillclimb-extract upload",
|
|
1204
|
+
"npx hillclimb-extract upload",
|
|
1205
|
+
"npx @hillclimb/extract upload"
|
|
1206
|
+
];
|
|
1207
|
+
}
|
|
1140
1208
|
function isHillclimbOwnedCommandFor(command, currentCommand) {
|
|
1141
1209
|
if (command === currentCommand || legacyCommandsFor(currentCommand).includes(command)) {
|
|
1142
1210
|
return true;
|
|
@@ -1152,7 +1220,7 @@ async function installHooksForTool(repoRoot, def) {
|
|
|
1152
1220
|
const r = await opencodeInstall(file);
|
|
1153
1221
|
return { settingsFile: file, ...r, changed: r.installed > 0 };
|
|
1154
1222
|
}
|
|
1155
|
-
const settings = await readJson(file);
|
|
1223
|
+
const settings = await readJson(file, { repairMalformed: true });
|
|
1156
1224
|
let installed = 0;
|
|
1157
1225
|
let alreadyPresent = 0;
|
|
1158
1226
|
let mutated = false;
|
|
@@ -1197,6 +1265,10 @@ async function checkHooksForTool(repoRoot, def) {
|
|
|
1197
1265
|
missing.push(`${evt.eventName}:${evt.command}`);
|
|
1198
1266
|
}
|
|
1199
1267
|
}
|
|
1268
|
+
if (def.tool === "codex" && missing.length === 0) {
|
|
1269
|
+
const enabled = await areCodexHooksEnabled();
|
|
1270
|
+
if (!enabled) missing.push("features.hooks");
|
|
1271
|
+
}
|
|
1200
1272
|
return { allInstalled: missing.length === 0, settingsFile: file, missing };
|
|
1201
1273
|
}
|
|
1202
1274
|
async function findLegacyGitTracesHookOwners(repoRoot, eventName) {
|
|
@@ -1220,9 +1292,57 @@ async function findLegacyGitTracesHookOwners(repoRoot, eventName) {
|
|
|
1220
1292
|
}
|
|
1221
1293
|
return owners;
|
|
1222
1294
|
}
|
|
1295
|
+
async function findLegacyUploadHookOwners(repoRoot, eventName) {
|
|
1296
|
+
if (!eventName) return [];
|
|
1297
|
+
const owners = [];
|
|
1298
|
+
for (const def of TOOLS) {
|
|
1299
|
+
if (def.format === "opencode") continue;
|
|
1300
|
+
const events = def.events.filter(
|
|
1301
|
+
(evt) => evt.eventName === eventName && evt.command.startsWith("npx hillclimb@latest upload")
|
|
1302
|
+
);
|
|
1303
|
+
if (events.length === 0) continue;
|
|
1304
|
+
const settings = await readJson(settingsPath(repoRoot, def));
|
|
1305
|
+
const commands = new Set(commandsForEvent(settings, def.format, eventName));
|
|
1306
|
+
if (events.some((evt) => {
|
|
1307
|
+
const candidates = evt.command.includes("--tool=") ? legacyBareUploadCommandsFor(evt.command) : [evt.command, ...legacyCommandsFor(evt.command)];
|
|
1308
|
+
return candidates.some((candidate) => commands.has(candidate));
|
|
1309
|
+
})) {
|
|
1310
|
+
owners.push(def.tool);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
return owners;
|
|
1314
|
+
}
|
|
1223
1315
|
function detectTools(repoRoot) {
|
|
1224
1316
|
return TOOLS.filter((def) => def.detect(repoRoot));
|
|
1225
1317
|
}
|
|
1318
|
+
function trackedHookFiles(repoRoot, files) {
|
|
1319
|
+
const rels = files.map((file) => path6.relative(repoRoot, file)).filter((rel) => rel && !rel.startsWith("..") && !path6.isAbsolute(rel));
|
|
1320
|
+
if (rels.length === 0) return [];
|
|
1321
|
+
try {
|
|
1322
|
+
const output = execFileSync("git", ["ls-files", "--", ...rels], {
|
|
1323
|
+
cwd: repoRoot,
|
|
1324
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1325
|
+
}).toString("utf-8").trim();
|
|
1326
|
+
if (!output) return [];
|
|
1327
|
+
return output.split(/\r?\n/).map((rel) => path6.join(repoRoot, rel));
|
|
1328
|
+
} catch {
|
|
1329
|
+
return [];
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
async function ensureHookFilesIgnored(repoRoot, files) {
|
|
1333
|
+
const ignored = await ensureGitignored(repoRoot, [
|
|
1334
|
+
...files,
|
|
1335
|
+
...files.map((file) => `${file}.malformed-*`)
|
|
1336
|
+
]);
|
|
1337
|
+
const tracked = trackedHookFiles(repoRoot, files);
|
|
1338
|
+
if (tracked.length > 0) {
|
|
1339
|
+
appendLog(
|
|
1340
|
+
"warn",
|
|
1341
|
+
`hooks: hook files are already tracked by git and may be committed (${tracked.join(", ")})`
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
return { ...ignored, tracked };
|
|
1345
|
+
}
|
|
1226
1346
|
async function installDetectedHooks(repoRoot) {
|
|
1227
1347
|
const detected = detectTools(repoRoot);
|
|
1228
1348
|
const results = [];
|
|
@@ -1250,6 +1370,20 @@ async function healHookForTool(repoRoot, tool) {
|
|
|
1250
1370
|
};
|
|
1251
1371
|
}
|
|
1252
1372
|
const r = await installHooksForTool(repoRoot, def);
|
|
1373
|
+
try {
|
|
1374
|
+
const ignored = await ensureHookFilesIgnored(repoRoot, [r.settingsFile]);
|
|
1375
|
+
if (ignored.changed) {
|
|
1376
|
+
appendLog(
|
|
1377
|
+
"info",
|
|
1378
|
+
`self-heal: gitignored ${def.tool} hook file (${ignored.added.join(",")})`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
appendLog(
|
|
1383
|
+
"warn",
|
|
1384
|
+
`self-heal: could not update .gitignore for ${def.tool} hook: ${err instanceof Error ? err.message : String(err)}`
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1253
1387
|
return {
|
|
1254
1388
|
tool: def.tool,
|
|
1255
1389
|
label: def.label,
|
|
@@ -1273,20 +1407,38 @@ async function checkAllHooks(repoRoot) {
|
|
|
1273
1407
|
}
|
|
1274
1408
|
return results;
|
|
1275
1409
|
}
|
|
1276
|
-
|
|
1410
|
+
function codexConfigPath() {
|
|
1411
|
+
return process.env.HILLCLIMB_CODEX_CONFIG_PATH ?? path6.join(os2.homedir(), ".codex", "config.toml");
|
|
1412
|
+
}
|
|
1413
|
+
function codexHooksEnabledInConfig(content) {
|
|
1414
|
+
const featuresMatch = content.match(/^\[features\]\s*$/m);
|
|
1415
|
+
if (!featuresMatch || featuresMatch.index === void 0) return false;
|
|
1416
|
+
const bodyStart = featuresMatch.index + featuresMatch[0].length;
|
|
1417
|
+
const nextSectionOffset = content.slice(bodyStart).search(/\n\[[^\n]+\]\s*(?:\r?\n|$)/);
|
|
1418
|
+
const bodyEnd = nextSectionOffset === -1 ? content.length : bodyStart + nextSectionOffset;
|
|
1419
|
+
const body = content.slice(bodyStart, bodyEnd);
|
|
1420
|
+
const hooksMatch = body.match(/^\s*hooks\s*=\s*(true|false)\s*(?:#.*)?$/im);
|
|
1421
|
+
return hooksMatch?.[1]?.toLowerCase() === "true";
|
|
1422
|
+
}
|
|
1423
|
+
async function areCodexHooksEnabled() {
|
|
1424
|
+
try {
|
|
1425
|
+
const content = await fs5.promises.readFile(codexConfigPath(), "utf-8");
|
|
1426
|
+
return codexHooksEnabledInConfig(content);
|
|
1427
|
+
} catch {
|
|
1428
|
+
return false;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1277
1431
|
async function ensureCodexHooksEnabled() {
|
|
1278
1432
|
let content;
|
|
1433
|
+
const configPath2 = codexConfigPath();
|
|
1279
1434
|
try {
|
|
1280
|
-
content = await fs5.promises.readFile(
|
|
1435
|
+
content = await fs5.promises.readFile(configPath2, "utf-8");
|
|
1281
1436
|
} catch (err) {
|
|
1282
1437
|
if (err.code === "ENOENT") {
|
|
1283
|
-
await fs5.promises.mkdir(path6.dirname(
|
|
1438
|
+
await fs5.promises.mkdir(path6.dirname(configPath2), {
|
|
1284
1439
|
recursive: true
|
|
1285
1440
|
});
|
|
1286
|
-
await fs5.promises.writeFile(
|
|
1287
|
-
CODEX_CONFIG_PATH,
|
|
1288
|
-
"[features]\nhooks = true\n"
|
|
1289
|
-
);
|
|
1441
|
+
await fs5.promises.writeFile(configPath2, "[features]\nhooks = true\n");
|
|
1290
1442
|
return true;
|
|
1291
1443
|
}
|
|
1292
1444
|
throw err;
|
|
@@ -1324,7 +1476,7 @@ hooks = true
|
|
|
1324
1476
|
`;
|
|
1325
1477
|
}
|
|
1326
1478
|
if (content === original) return false;
|
|
1327
|
-
await fs5.promises.writeFile(
|
|
1479
|
+
await fs5.promises.writeFile(configPath2, content);
|
|
1328
1480
|
return true;
|
|
1329
1481
|
}
|
|
1330
1482
|
var CLAUDE_DEF = TOOLS[0];
|
|
@@ -1724,6 +1876,7 @@ async function runInit(args = []) {
|
|
|
1724
1876
|
userId: bootstrap.session.userId,
|
|
1725
1877
|
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1726
1878
|
});
|
|
1879
|
+
const autoUploadWarnings = [];
|
|
1727
1880
|
try {
|
|
1728
1881
|
const hookResults = await installDetectedHooks(repoRoot);
|
|
1729
1882
|
if (hookResults.length === 0) {
|
|
@@ -1748,7 +1901,7 @@ async function runInit(args = []) {
|
|
|
1748
1901
|
}
|
|
1749
1902
|
}
|
|
1750
1903
|
try {
|
|
1751
|
-
const ignored = await
|
|
1904
|
+
const ignored = await ensureHookFilesIgnored(
|
|
1752
1905
|
repoRoot,
|
|
1753
1906
|
hookResults.map((r) => r.settingsFile)
|
|
1754
1907
|
);
|
|
@@ -1761,11 +1914,21 @@ async function runInit(args = []) {
|
|
|
1761
1914
|
`${ignored.created ? "Created" : "Updated"} .gitignore for hook files`
|
|
1762
1915
|
);
|
|
1763
1916
|
}
|
|
1917
|
+
if (ignored.tracked.length > 0) {
|
|
1918
|
+
const tracked = ignored.tracked.map((file) => path7.relative(repoRoot, file)).join(", ");
|
|
1919
|
+
autoUploadWarnings.push(
|
|
1920
|
+
`Hook files are already tracked by git: ${tracked}`
|
|
1921
|
+
);
|
|
1922
|
+
p3.log.warn(
|
|
1923
|
+
`Hook files are already tracked by git and may be committed: ${tracked}`
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1764
1926
|
} catch (err) {
|
|
1765
1927
|
appendLog(
|
|
1766
1928
|
"warn",
|
|
1767
1929
|
`init: could not update .gitignore for hook files: ${formatError(err)}`
|
|
1768
1930
|
);
|
|
1931
|
+
autoUploadWarnings.push("Could not update .gitignore for hook files.");
|
|
1769
1932
|
p3.log.warn(
|
|
1770
1933
|
`Could not update .gitignore for hook files: ${err instanceof Error ? err.message : String(err)}`
|
|
1771
1934
|
);
|
|
@@ -1782,6 +1945,9 @@ async function runInit(args = []) {
|
|
|
1782
1945
|
"warn",
|
|
1783
1946
|
`init: could not enable Codex hooks: ${formatError(err)}`
|
|
1784
1947
|
);
|
|
1948
|
+
autoUploadWarnings.push(
|
|
1949
|
+
"Could not enable Codex hooks in config.toml."
|
|
1950
|
+
);
|
|
1785
1951
|
p3.log.warn(
|
|
1786
1952
|
`Could not enable hooks in config.toml: ${err instanceof Error ? err.message : String(err)}`
|
|
1787
1953
|
);
|
|
@@ -1801,7 +1967,7 @@ async function runInit(args = []) {
|
|
|
1801
1967
|
`init: completed (project=${project.slug}, contributionType=${type.slug})`
|
|
1802
1968
|
);
|
|
1803
1969
|
p3.outro(
|
|
1804
|
-
`Done. Your next coding session in this repo will upload automatically to ${project.name}.`
|
|
1970
|
+
autoUploadWarnings.length > 0 ? `Configured ${project.name}, but auto-upload needs attention: ${autoUploadWarnings.join(" ")}` : `Done. Your next coding session in this repo will upload automatically to ${project.name}.`
|
|
1805
1971
|
);
|
|
1806
1972
|
}
|
|
1807
1973
|
|
|
@@ -2020,7 +2186,7 @@ async function runLogout(args = []) {
|
|
|
2020
2186
|
}
|
|
2021
2187
|
|
|
2022
2188
|
// src/commands/status.ts
|
|
2023
|
-
import
|
|
2189
|
+
import path8 from "path";
|
|
2024
2190
|
async function runStatus(args = []) {
|
|
2025
2191
|
const debug = args.includes("--debug");
|
|
2026
2192
|
header("status");
|
|
@@ -2038,7 +2204,7 @@ async function runStatus(args = []) {
|
|
|
2038
2204
|
return;
|
|
2039
2205
|
}
|
|
2040
2206
|
const { repoRoot, config } = match;
|
|
2041
|
-
const repoName = repo?.name ??
|
|
2207
|
+
const repoName = repo?.name ?? path8.basename(repoRoot);
|
|
2042
2208
|
if (IS_TTY) {
|
|
2043
2209
|
process.stdout.write(` ${dim("Checking login...")}`);
|
|
2044
2210
|
}
|
|
@@ -2132,7 +2298,7 @@ async function runStatus(args = []) {
|
|
|
2132
2298
|
try {
|
|
2133
2299
|
const hookStatuses = await checkAllHooks(repoRoot);
|
|
2134
2300
|
for (const h of hookStatuses) {
|
|
2135
|
-
const rel =
|
|
2301
|
+
const rel = path8.relative(repoRoot, h.settingsFile) || h.settingsFile;
|
|
2136
2302
|
debugRow(
|
|
2137
2303
|
`${h.label} hook:`,
|
|
2138
2304
|
`${rel} ${dim(`(${h.installed ? "installed" : "missing"})`)}`
|
|
@@ -2149,14 +2315,67 @@ async function runStatus(args = []) {
|
|
|
2149
2315
|
|
|
2150
2316
|
// src/commands/upload.ts
|
|
2151
2317
|
import { spawn as spawn2 } from "child_process";
|
|
2152
|
-
import
|
|
2153
|
-
import
|
|
2154
|
-
import
|
|
2318
|
+
import crypto3 from "crypto";
|
|
2319
|
+
import fs11 from "fs";
|
|
2320
|
+
import os6 from "os";
|
|
2321
|
+
import path15 from "path";
|
|
2155
2322
|
|
|
2156
2323
|
// src/debug-logs.ts
|
|
2157
2324
|
import crypto from "crypto";
|
|
2158
2325
|
import fs9 from "fs";
|
|
2159
|
-
import
|
|
2326
|
+
import path12 from "path";
|
|
2327
|
+
|
|
2328
|
+
// src/hook-events.ts
|
|
2329
|
+
function classifyHookEvent(event) {
|
|
2330
|
+
switch (event) {
|
|
2331
|
+
case "Stop":
|
|
2332
|
+
case "stop":
|
|
2333
|
+
case "session.idle":
|
|
2334
|
+
return "stop";
|
|
2335
|
+
case "SessionEnd":
|
|
2336
|
+
case "sessionEnd":
|
|
2337
|
+
case "session.deleted":
|
|
2338
|
+
case "server.instance.disposed":
|
|
2339
|
+
return "sessionEnd";
|
|
2340
|
+
default:
|
|
2341
|
+
return null;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// src/hook-payload.ts
|
|
2346
|
+
function resolveHookCwd(payload) {
|
|
2347
|
+
return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
|
|
2348
|
+
}
|
|
2349
|
+
function resolveHookSessionId(payload) {
|
|
2350
|
+
return payload.session_id ?? payload.conversation_id ?? null;
|
|
2351
|
+
}
|
|
2352
|
+
function inferSourceToolFromPayload(payload) {
|
|
2353
|
+
if (payload.tool) return payload.tool;
|
|
2354
|
+
if (payload.cursor_version) return "cursor";
|
|
2355
|
+
const transcriptPath = payload.transcript_path ?? "";
|
|
2356
|
+
if (transcriptPath.includes(".claude/projects/")) return "claude";
|
|
2357
|
+
if (transcriptPath.includes(".cursor/projects/")) return "cursor";
|
|
2358
|
+
if (transcriptPath.includes("GitHub.copilot-chat")) return "copilot-chat";
|
|
2359
|
+
if (transcriptPath.includes(".hillclimb/opencode-transcripts/")) {
|
|
2360
|
+
return "opencode";
|
|
2361
|
+
}
|
|
2362
|
+
if (transcriptPath.includes(".codex/")) return "codex";
|
|
2363
|
+
switch (payload.hook_event_name) {
|
|
2364
|
+
case "session.idle":
|
|
2365
|
+
case "session.deleted":
|
|
2366
|
+
case "server.instance.disposed":
|
|
2367
|
+
return "opencode";
|
|
2368
|
+
default:
|
|
2369
|
+
return null;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
function fallbackSourceTool(payload) {
|
|
2373
|
+
const inferred = inferSourceToolFromPayload(payload);
|
|
2374
|
+
if (inferred) return inferred;
|
|
2375
|
+
const eventKind = classifyHookEvent(payload.hook_event_name);
|
|
2376
|
+
if (eventKind === "stop") return "codex";
|
|
2377
|
+
return "claude";
|
|
2378
|
+
}
|
|
2160
2379
|
|
|
2161
2380
|
// src/middleware/pattern-redact.ts
|
|
2162
2381
|
import os3 from "os";
|
|
@@ -10645,7 +10864,7 @@ var middleware = [];
|
|
|
10645
10864
|
|
|
10646
10865
|
// src/middleware/secrets.ts
|
|
10647
10866
|
import fs7 from "fs";
|
|
10648
|
-
import
|
|
10867
|
+
import path9 from "path";
|
|
10649
10868
|
var KNOWN_NON_SECRETS = /* @__PURE__ */ new Set([
|
|
10650
10869
|
"true",
|
|
10651
10870
|
"false",
|
|
@@ -10800,7 +11019,7 @@ async function discoverEnvFiles(repoRoot) {
|
|
|
10800
11019
|
const envFiles = [];
|
|
10801
11020
|
for (const name of entries) {
|
|
10802
11021
|
if (!name.startsWith(".env")) continue;
|
|
10803
|
-
const filePath =
|
|
11022
|
+
const filePath = path9.join(repoRoot, name);
|
|
10804
11023
|
try {
|
|
10805
11024
|
const stat = await fs7.promises.stat(filePath);
|
|
10806
11025
|
if (stat.isFile()) envFiles.push(name);
|
|
@@ -10825,7 +11044,7 @@ async function collectSecrets(repoRoot, envFiles, additionalFiles) {
|
|
|
10825
11044
|
}
|
|
10826
11045
|
}
|
|
10827
11046
|
for (const filePath of additionalFiles) {
|
|
10828
|
-
const resolved =
|
|
11047
|
+
const resolved = path9.resolve(repoRoot, filePath);
|
|
10829
11048
|
sourceFiles.push(resolved);
|
|
10830
11049
|
for (const value of await parseEnvFile(resolved)) {
|
|
10831
11050
|
if (isUsableValue(value)) {
|
|
@@ -10855,16 +11074,16 @@ import archiver from "archiver";
|
|
|
10855
11074
|
|
|
10856
11075
|
// src/outputs/archive.ts
|
|
10857
11076
|
import os4 from "os";
|
|
10858
|
-
import
|
|
11077
|
+
import path10 from "path";
|
|
10859
11078
|
function getSourceBaseDir(sourceName) {
|
|
10860
11079
|
const home = os4.homedir();
|
|
10861
11080
|
switch (sourceName) {
|
|
10862
11081
|
case "claude":
|
|
10863
|
-
return
|
|
11082
|
+
return path10.join(home, ".claude", "projects");
|
|
10864
11083
|
case "codex":
|
|
10865
|
-
return
|
|
11084
|
+
return path10.join(home, ".codex", "sessions");
|
|
10866
11085
|
case "debug-logs":
|
|
10867
|
-
return
|
|
11086
|
+
return path10.join(configDir(), "logs");
|
|
10868
11087
|
default:
|
|
10869
11088
|
return home;
|
|
10870
11089
|
}
|
|
@@ -10873,8 +11092,8 @@ function addGroupToArchive(archive, group, selectedSources) {
|
|
|
10873
11092
|
for (const file of group.files) {
|
|
10874
11093
|
if (!selectedSources.has(file.sourceName)) continue;
|
|
10875
11094
|
const baseDir = getSourceBaseDir(file.sourceName);
|
|
10876
|
-
const relativePath = file.absolutePath.startsWith(baseDir) ?
|
|
10877
|
-
const archivePath =
|
|
11095
|
+
const relativePath = file.absolutePath.startsWith(baseDir) ? path10.relative(baseDir, file.absolutePath) : path10.basename(file.absolutePath);
|
|
11096
|
+
const archivePath = path10.join(file.sourceName, relativePath);
|
|
10878
11097
|
if (file.content) {
|
|
10879
11098
|
archive.append(file.content, { name: archivePath });
|
|
10880
11099
|
} else {
|
|
@@ -10916,14 +11135,23 @@ var PlatformUploadOutput = class {
|
|
|
10916
11135
|
contributionTitle,
|
|
10917
11136
|
contributionBody,
|
|
10918
11137
|
zipFilename,
|
|
10919
|
-
autoSubmit
|
|
11138
|
+
autoSubmit,
|
|
11139
|
+
existingContributionId,
|
|
11140
|
+
onContributionCreated
|
|
10920
11141
|
} = this.opts;
|
|
10921
|
-
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
|
|
10925
|
-
|
|
10926
|
-
|
|
11142
|
+
let contributionId;
|
|
11143
|
+
if (existingContributionId) {
|
|
11144
|
+
contributionId = existingContributionId;
|
|
11145
|
+
} else {
|
|
11146
|
+
const contribution = await client.createContribution(projectId, {
|
|
11147
|
+
contributionTypeSlug,
|
|
11148
|
+
title: contributionTitle,
|
|
11149
|
+
body: contributionBody
|
|
11150
|
+
});
|
|
11151
|
+
contributionId = contribution.id;
|
|
11152
|
+
if (onContributionCreated) await onContributionCreated(contributionId);
|
|
11153
|
+
}
|
|
11154
|
+
const presigned = await client.createUpload(contributionId, {
|
|
10927
11155
|
originalFilename: zipFilename,
|
|
10928
11156
|
mimeType: "application/zip",
|
|
10929
11157
|
sizeBytes: buffer.byteLength
|
|
@@ -10939,30 +11167,30 @@ var PlatformUploadOutput = class {
|
|
|
10939
11167
|
);
|
|
10940
11168
|
appendLog("info", `PUT to presigned URL succeeded for ${zipFilename}`);
|
|
10941
11169
|
if (autoSubmit) {
|
|
10942
|
-
appendLog("info", `submitting contribution ${
|
|
10943
|
-
await client.submitContribution(
|
|
10944
|
-
appendLog("info", `contribution ${
|
|
11170
|
+
appendLog("info", `submitting contribution ${contributionId}`);
|
|
11171
|
+
await client.submitContribution(contributionId);
|
|
11172
|
+
appendLog("info", `contribution ${contributionId} submitted`);
|
|
10945
11173
|
}
|
|
10946
|
-
return
|
|
11174
|
+
return contributionId;
|
|
10947
11175
|
}
|
|
10948
11176
|
};
|
|
10949
11177
|
|
|
10950
11178
|
// src/pipeline.ts
|
|
10951
11179
|
import fs8 from "fs";
|
|
10952
|
-
import
|
|
11180
|
+
import path11 from "path";
|
|
10953
11181
|
function canonicalizePath(p7) {
|
|
10954
|
-
let resolved =
|
|
10955
|
-
if (resolved.endsWith(
|
|
11182
|
+
let resolved = path11.resolve(p7);
|
|
11183
|
+
if (resolved.endsWith(path11.sep) && resolved !== path11.sep) {
|
|
10956
11184
|
resolved = resolved.slice(0, -1);
|
|
10957
11185
|
}
|
|
10958
11186
|
return resolved;
|
|
10959
11187
|
}
|
|
10960
11188
|
function computeLabel(repoPath, allPaths) {
|
|
10961
|
-
const segments = repoPath.split(
|
|
11189
|
+
const segments = repoPath.split(path11.sep).filter(Boolean);
|
|
10962
11190
|
for (let depth = 1; depth <= segments.length; depth++) {
|
|
10963
11191
|
const label = segments.slice(-depth).join("/");
|
|
10964
11192
|
const matches = allPaths.filter((p7) => {
|
|
10965
|
-
const s = p7.split(
|
|
11193
|
+
const s = p7.split(path11.sep).filter(Boolean);
|
|
10966
11194
|
return s.slice(-depth).join("/") === label;
|
|
10967
11195
|
});
|
|
10968
11196
|
if (matches.length === 1) return label;
|
|
@@ -11040,7 +11268,7 @@ var DEFAULT_WAIT_MS = 6e4;
|
|
|
11040
11268
|
var LOCK_RETRIES = 100;
|
|
11041
11269
|
var LOCK_RETRY_DELAY_MS = 100;
|
|
11042
11270
|
function stateDir() {
|
|
11043
|
-
return process.env.HILLCLIMB_DEBUG_LOG_STATE_DIR ??
|
|
11271
|
+
return process.env.HILLCLIMB_DEBUG_LOG_STATE_DIR ?? path12.join(configDir(), "debug-log-uploads");
|
|
11044
11272
|
}
|
|
11045
11273
|
function waitMs() {
|
|
11046
11274
|
const raw = process.env.HILLCLIMB_DEBUG_LOG_WAIT_MS;
|
|
@@ -11049,7 +11277,7 @@ function waitMs() {
|
|
|
11049
11277
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_WAIT_MS;
|
|
11050
11278
|
}
|
|
11051
11279
|
function stateFile(eventId) {
|
|
11052
|
-
return
|
|
11280
|
+
return path12.join(stateDir(), `${eventId}.json`);
|
|
11053
11281
|
}
|
|
11054
11282
|
function lockFile(eventId) {
|
|
11055
11283
|
return `${stateFile(eventId)}.lock`;
|
|
@@ -11073,26 +11301,11 @@ function toolLabel(tool) {
|
|
|
11073
11301
|
};
|
|
11074
11302
|
return labels[tool] ?? tool;
|
|
11075
11303
|
}
|
|
11076
|
-
function classifyHookEvent(event) {
|
|
11077
|
-
switch (event) {
|
|
11078
|
-
case "Stop":
|
|
11079
|
-
case "stop":
|
|
11080
|
-
case "session.idle":
|
|
11081
|
-
return "stop";
|
|
11082
|
-
case "SessionEnd":
|
|
11083
|
-
case "sessionEnd":
|
|
11084
|
-
case "session.deleted":
|
|
11085
|
-
case "server.instance.disposed":
|
|
11086
|
-
return "sessionEnd";
|
|
11087
|
-
default:
|
|
11088
|
-
return null;
|
|
11089
|
-
}
|
|
11090
|
-
}
|
|
11091
11304
|
function resolveCwd(payload) {
|
|
11092
|
-
return payload
|
|
11305
|
+
return resolveHookCwd(payload);
|
|
11093
11306
|
}
|
|
11094
11307
|
function resolveSessionId(payload) {
|
|
11095
|
-
return payload
|
|
11308
|
+
return resolveHookSessionId(payload);
|
|
11096
11309
|
}
|
|
11097
11310
|
function stringOrNull(value) {
|
|
11098
11311
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
@@ -11100,7 +11313,7 @@ function stringOrNull(value) {
|
|
|
11100
11313
|
function expectedKinds(tool, eventKind, payload) {
|
|
11101
11314
|
if (eventKind === "stop") {
|
|
11102
11315
|
return new Set(
|
|
11103
|
-
tool === "codex" || tool === "copilot-chat" ? ["agent", "git"] : ["git"]
|
|
11316
|
+
tool === "codex" || tool === "copilot-chat" || tool === "claude" || tool === "cursor" || tool === "opencode" ? ["agent", "git"] : ["git"]
|
|
11104
11317
|
);
|
|
11105
11318
|
}
|
|
11106
11319
|
if (tool === "opencode" && !resolveSessionId(payload)) {
|
|
@@ -11111,7 +11324,7 @@ function expectedKinds(tool, eventKind, payload) {
|
|
|
11111
11324
|
async function transcriptFingerprint(payload) {
|
|
11112
11325
|
const transcriptPath = stringOrNull(payload.transcript_path);
|
|
11113
11326
|
if (!transcriptPath) return {};
|
|
11114
|
-
const resolved =
|
|
11327
|
+
const resolved = path12.resolve(transcriptPath);
|
|
11115
11328
|
try {
|
|
11116
11329
|
const stat = await fs9.promises.stat(resolved);
|
|
11117
11330
|
return {
|
|
@@ -11150,13 +11363,10 @@ async function eventContext(tool, payload) {
|
|
|
11150
11363
|
conversationId: stringOrNull(payload.conversation_id),
|
|
11151
11364
|
turnId,
|
|
11152
11365
|
eventNonce,
|
|
11153
|
-
// Only
|
|
11154
|
-
//
|
|
11155
|
-
//
|
|
11156
|
-
|
|
11157
|
-
// transcript_path to the upload spawn but not the git-traces spawn,
|
|
11158
|
-
// causing a 60s orphan wait and a duplicate debug-log upload.
|
|
11159
|
-
...eventKind === "stop" ? await transcriptFingerprint(payload) : {}
|
|
11366
|
+
// Only stop events without stable turn IDs need the transcript to
|
|
11367
|
+
// disambiguate. When a turn_id exists, including mutable transcript
|
|
11368
|
+
// mtime/size can split agent+git completions for the same logical event.
|
|
11369
|
+
...eventKind === "stop" && !turnId ? await transcriptFingerprint(payload) : {}
|
|
11160
11370
|
};
|
|
11161
11371
|
const eventId = crypto.createHash("sha256").update(JSON.stringify(fingerprint)).digest("hex").slice(0, 32);
|
|
11162
11372
|
return {
|
|
@@ -11227,7 +11437,7 @@ function initialState(ctx, now) {
|
|
|
11227
11437
|
eventKind: ctx.eventKind,
|
|
11228
11438
|
hookEventName: ctx.hookEventName,
|
|
11229
11439
|
sessionId: ctx.sessionId,
|
|
11230
|
-
logDate:
|
|
11440
|
+
logDate: path12.basename(todayLogPath(), ".log"),
|
|
11231
11441
|
firstSeenAt: now.toISOString()
|
|
11232
11442
|
};
|
|
11233
11443
|
}
|
|
@@ -11289,7 +11499,7 @@ async function waitForExpectedKinds(ctx, state) {
|
|
|
11289
11499
|
}
|
|
11290
11500
|
async function buildMiddleware(repoRoot) {
|
|
11291
11501
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
11292
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
11502
|
+
const envFilePaths = envFileNames.map((n) => path12.join(repoRoot, n));
|
|
11293
11503
|
const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
|
|
11294
11504
|
const middleware2 = [];
|
|
11295
11505
|
if (secretResult.values.size > 0) {
|
|
@@ -11335,7 +11545,7 @@ async function uploadDebugLog(ctx, state) {
|
|
|
11335
11545
|
};
|
|
11336
11546
|
const group = {
|
|
11337
11547
|
repoPath: ctx.repoRoot,
|
|
11338
|
-
label:
|
|
11548
|
+
label: path12.basename(ctx.repoRoot),
|
|
11339
11549
|
files: [sourceFile],
|
|
11340
11550
|
sourceNames: [DEBUG_LOGS_SLUG],
|
|
11341
11551
|
lastModified: now
|
|
@@ -11357,7 +11567,7 @@ async function uploadDebugLog(ctx, state) {
|
|
|
11357
11567
|
`Tool: ${label}`,
|
|
11358
11568
|
`Event: ${ctx.hookEventName ?? ctx.eventKind}`,
|
|
11359
11569
|
`Repo: ${ctx.repoRoot}`,
|
|
11360
|
-
`Log: ${
|
|
11570
|
+
`Log: ${path12.basename(logPath)}`,
|
|
11361
11571
|
`Agent done: ${state.agentDoneAt ?? "<not observed>"}`,
|
|
11362
11572
|
`Git done: ${state.gitDoneAt ?? "<not observed>"}`,
|
|
11363
11573
|
`Uploaded: ${now.toISOString()}`
|
|
@@ -11374,7 +11584,7 @@ async function uploadDebugLog(ctx, state) {
|
|
|
11374
11584
|
);
|
|
11375
11585
|
appendLog(
|
|
11376
11586
|
"info",
|
|
11377
|
-
`debug-logs: uploaded ${
|
|
11587
|
+
`debug-logs: uploaded ${path12.basename(logPath)} to project ${ctx.config.projectSlug} (${ctx.config.projectId}) as contribution ${contributionId}`
|
|
11378
11588
|
);
|
|
11379
11589
|
return contributionId;
|
|
11380
11590
|
} catch (err) {
|
|
@@ -11427,7 +11637,7 @@ async function recordDebugLogCompletion(args) {
|
|
|
11427
11637
|
}
|
|
11428
11638
|
|
|
11429
11639
|
// src/normalizer/index.ts
|
|
11430
|
-
import
|
|
11640
|
+
import path13 from "path";
|
|
11431
11641
|
|
|
11432
11642
|
// src/normalizer/claude.ts
|
|
11433
11643
|
function stringify(value) {
|
|
@@ -13125,14 +13335,20 @@ var NormalizeMiddleware = class {
|
|
|
13125
13335
|
continue;
|
|
13126
13336
|
const content = file.content ? file.content.toString("utf-8") : null;
|
|
13127
13337
|
if (!content) continue;
|
|
13128
|
-
const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 :
|
|
13338
|
+
const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 : path13.basename(file.absolutePath, ".jsonl"));
|
|
13129
13339
|
try {
|
|
13130
13340
|
const trajectory = normalizeContent(
|
|
13131
13341
|
file.sourceName,
|
|
13132
13342
|
content,
|
|
13133
13343
|
sessionId
|
|
13134
13344
|
);
|
|
13135
|
-
if (!trajectory)
|
|
13345
|
+
if (!trajectory) {
|
|
13346
|
+
appendLog(
|
|
13347
|
+
"warn",
|
|
13348
|
+
`normalize: produced no ATIF for source=${file.sourceName} session=${sessionId ?? "<unknown>"} file=${file.absolutePath}`
|
|
13349
|
+
);
|
|
13350
|
+
continue;
|
|
13351
|
+
}
|
|
13136
13352
|
const json = JSON.stringify(
|
|
13137
13353
|
excludeNone(trajectory),
|
|
13138
13354
|
null,
|
|
@@ -13146,13 +13362,140 @@ var NormalizeMiddleware = class {
|
|
|
13146
13362
|
metadata: { ...file.metadata, isAtif: true },
|
|
13147
13363
|
content: Buffer.from(json, "utf-8")
|
|
13148
13364
|
});
|
|
13149
|
-
} catch {
|
|
13150
|
-
|
|
13365
|
+
} catch (err) {
|
|
13366
|
+
appendLog(
|
|
13367
|
+
"warn",
|
|
13368
|
+
`normalize: failed for source=${file.sourceName} session=${sessionId ?? "<unknown>"} file=${file.absolutePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
13369
|
+
);
|
|
13370
|
+
}
|
|
13151
13371
|
}
|
|
13152
13372
|
return { ...group, files: newFiles };
|
|
13153
13373
|
}
|
|
13154
13374
|
};
|
|
13155
13375
|
|
|
13376
|
+
// src/upload-state.ts
|
|
13377
|
+
import crypto2 from "crypto";
|
|
13378
|
+
import fs10 from "fs";
|
|
13379
|
+
import os5 from "os";
|
|
13380
|
+
import path14 from "path";
|
|
13381
|
+
var CURRENT_SCHEMA_VERSION2 = 1;
|
|
13382
|
+
var DEFAULT_STATE_DIR = path14.join(
|
|
13383
|
+
os5.homedir(),
|
|
13384
|
+
".hillclimb",
|
|
13385
|
+
"agent-uploads"
|
|
13386
|
+
);
|
|
13387
|
+
var LOCK_RETRIES2 = 120;
|
|
13388
|
+
var LOCK_RETRY_DELAY_MS2 = 500;
|
|
13389
|
+
var DEFAULT_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
13390
|
+
function stateDir2() {
|
|
13391
|
+
return process.env.HILLCLIMB_UPLOAD_STATE_DIR ?? DEFAULT_STATE_DIR;
|
|
13392
|
+
}
|
|
13393
|
+
function readPositiveEnvMs(name, fallback) {
|
|
13394
|
+
const raw = process.env[name];
|
|
13395
|
+
if (raw === void 0) return fallback;
|
|
13396
|
+
const n = Number(raw);
|
|
13397
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
13398
|
+
}
|
|
13399
|
+
function stateTtlMs() {
|
|
13400
|
+
return readPositiveEnvMs(
|
|
13401
|
+
"HILLCLIMB_UPLOAD_STATE_TTL_MS",
|
|
13402
|
+
DEFAULT_STATE_TTL_MS
|
|
13403
|
+
);
|
|
13404
|
+
}
|
|
13405
|
+
function stateFileFor(repoRoot, tool, sessionId) {
|
|
13406
|
+
const hash = crypto2.createHash("sha256").update(`${path14.resolve(repoRoot)}\0${tool}\0${sessionId}`).digest("hex").slice(0, 16);
|
|
13407
|
+
return path14.join(stateDir2(), `${hash}.json`);
|
|
13408
|
+
}
|
|
13409
|
+
function lockFileFor(repoRoot, tool, sessionId) {
|
|
13410
|
+
return `${stateFileFor(repoRoot, tool, sessionId)}.lock`;
|
|
13411
|
+
}
|
|
13412
|
+
async function readUploadState(repoRoot, tool, sessionId) {
|
|
13413
|
+
try {
|
|
13414
|
+
const raw = await fs10.promises.readFile(
|
|
13415
|
+
stateFileFor(repoRoot, tool, sessionId),
|
|
13416
|
+
"utf-8"
|
|
13417
|
+
);
|
|
13418
|
+
const parsed = JSON.parse(raw);
|
|
13419
|
+
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION2) return null;
|
|
13420
|
+
return parsed;
|
|
13421
|
+
} catch {
|
|
13422
|
+
return null;
|
|
13423
|
+
}
|
|
13424
|
+
}
|
|
13425
|
+
async function writeUploadState(state) {
|
|
13426
|
+
const file = stateFileFor(state.repoRoot, state.tool, state.sessionId);
|
|
13427
|
+
await fs10.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
|
|
13428
|
+
const tmp = `${file}.tmp`;
|
|
13429
|
+
await fs10.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
13430
|
+
mode: 384
|
|
13431
|
+
});
|
|
13432
|
+
await fs10.promises.rename(tmp, file);
|
|
13433
|
+
}
|
|
13434
|
+
async function deleteUploadState(repoRoot, tool, sessionId) {
|
|
13435
|
+
try {
|
|
13436
|
+
await fs10.promises.unlink(stateFileFor(repoRoot, tool, sessionId));
|
|
13437
|
+
} catch {
|
|
13438
|
+
}
|
|
13439
|
+
}
|
|
13440
|
+
async function acquireLock2(repoRoot, tool, sessionId, retries = LOCK_RETRIES2, delayMs = LOCK_RETRY_DELAY_MS2) {
|
|
13441
|
+
const lockPath = lockFileFor(repoRoot, tool, sessionId);
|
|
13442
|
+
await fs10.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
|
|
13443
|
+
for (let i = 0; i < retries; i++) {
|
|
13444
|
+
try {
|
|
13445
|
+
const fd = await fs10.promises.open(
|
|
13446
|
+
lockPath,
|
|
13447
|
+
fs10.constants.O_CREAT | fs10.constants.O_EXCL | fs10.constants.O_WRONLY
|
|
13448
|
+
);
|
|
13449
|
+
try {
|
|
13450
|
+
await fd.write(String(process.pid));
|
|
13451
|
+
} finally {
|
|
13452
|
+
await fd.close();
|
|
13453
|
+
}
|
|
13454
|
+
return;
|
|
13455
|
+
} catch (err) {
|
|
13456
|
+
if (err.code === "EEXIST" && i < retries - 1) {
|
|
13457
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
13458
|
+
continue;
|
|
13459
|
+
}
|
|
13460
|
+
throw err;
|
|
13461
|
+
}
|
|
13462
|
+
}
|
|
13463
|
+
throw new Error(`Failed to acquire upload lock after ${retries} retries`);
|
|
13464
|
+
}
|
|
13465
|
+
async function releaseLock2(repoRoot, tool, sessionId) {
|
|
13466
|
+
try {
|
|
13467
|
+
await fs10.promises.unlink(lockFileFor(repoRoot, tool, sessionId));
|
|
13468
|
+
} catch {
|
|
13469
|
+
}
|
|
13470
|
+
}
|
|
13471
|
+
async function withUploadLock(repoRoot, tool, sessionId, fn) {
|
|
13472
|
+
await acquireLock2(repoRoot, tool, sessionId);
|
|
13473
|
+
try {
|
|
13474
|
+
return await fn();
|
|
13475
|
+
} finally {
|
|
13476
|
+
await releaseLock2(repoRoot, tool, sessionId);
|
|
13477
|
+
}
|
|
13478
|
+
}
|
|
13479
|
+
async function sweepStaleUploadStates(ttlMs = stateTtlMs(), now = Date.now()) {
|
|
13480
|
+
let entries;
|
|
13481
|
+
try {
|
|
13482
|
+
entries = await fs10.promises.readdir(stateDir2(), { withFileTypes: true });
|
|
13483
|
+
} catch {
|
|
13484
|
+
return;
|
|
13485
|
+
}
|
|
13486
|
+
for (const entry of entries) {
|
|
13487
|
+
if (!entry.isFile()) continue;
|
|
13488
|
+
const file = path14.join(stateDir2(), entry.name);
|
|
13489
|
+
try {
|
|
13490
|
+
const st = await fs10.promises.stat(file);
|
|
13491
|
+
if (now - st.mtimeMs > ttlMs) {
|
|
13492
|
+
await fs10.promises.unlink(file);
|
|
13493
|
+
}
|
|
13494
|
+
} catch {
|
|
13495
|
+
}
|
|
13496
|
+
}
|
|
13497
|
+
}
|
|
13498
|
+
|
|
13156
13499
|
// src/commands/upload.ts
|
|
13157
13500
|
async function readStdin() {
|
|
13158
13501
|
if (process.stdin.isTTY) return "";
|
|
@@ -13168,11 +13511,14 @@ function sanitize2(value) {
|
|
|
13168
13511
|
function formatEpochSeconds2(date) {
|
|
13169
13512
|
return String(Math.floor(date.getTime() / 1e3));
|
|
13170
13513
|
}
|
|
13514
|
+
function newFlowId() {
|
|
13515
|
+
return crypto3.randomBytes(3).toString("hex");
|
|
13516
|
+
}
|
|
13171
13517
|
function lineHasAssistant(line) {
|
|
13172
13518
|
return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
|
|
13173
13519
|
}
|
|
13174
13520
|
async function hasAssistantMessage(transcriptPath) {
|
|
13175
|
-
const stream =
|
|
13521
|
+
const stream = fs11.createReadStream(transcriptPath, { encoding: "utf-8" });
|
|
13176
13522
|
let buffer = "";
|
|
13177
13523
|
try {
|
|
13178
13524
|
for await (const chunk of stream) {
|
|
@@ -13194,16 +13540,41 @@ async function hasAssistantMessage(transcriptPath) {
|
|
|
13194
13540
|
}
|
|
13195
13541
|
return false;
|
|
13196
13542
|
}
|
|
13197
|
-
function
|
|
13198
|
-
|
|
13199
|
-
|
|
13200
|
-
|
|
13201
|
-
|
|
13543
|
+
function hashFileSha256(file) {
|
|
13544
|
+
return new Promise((resolve, reject) => {
|
|
13545
|
+
const hash = crypto3.createHash("sha256");
|
|
13546
|
+
const stream = fs11.createReadStream(file);
|
|
13547
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
13548
|
+
stream.on("error", reject);
|
|
13549
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
13550
|
+
});
|
|
13551
|
+
}
|
|
13552
|
+
async function resolveSourceTool(payload, repoRoot) {
|
|
13553
|
+
const inferred = inferSourceToolFromPayload(payload);
|
|
13554
|
+
if (inferred) return inferred;
|
|
13555
|
+
if (repoRoot) {
|
|
13556
|
+
const owners = await findLegacyUploadHookOwners(
|
|
13557
|
+
repoRoot,
|
|
13558
|
+
payload.hook_event_name
|
|
13559
|
+
);
|
|
13560
|
+
if (owners.length === 1) {
|
|
13561
|
+
appendLog("info", `upload: inferred legacy bare hook owner ${owners[0]}`);
|
|
13562
|
+
return owners[0];
|
|
13563
|
+
}
|
|
13564
|
+
if (owners.length > 1) {
|
|
13565
|
+
appendLog(
|
|
13566
|
+
"warn",
|
|
13567
|
+
`upload: legacy bare hook matched multiple tools (${owners.join(", ")}); skipping to avoid misattribution`
|
|
13568
|
+
);
|
|
13569
|
+
return null;
|
|
13570
|
+
}
|
|
13571
|
+
}
|
|
13572
|
+
return fallbackSourceTool(payload);
|
|
13202
13573
|
}
|
|
13203
13574
|
function summarizePayload(payload) {
|
|
13204
|
-
const sessionId = payload
|
|
13575
|
+
const sessionId = resolveHookSessionId(payload);
|
|
13205
13576
|
const turnId = payload.turn_id ?? null;
|
|
13206
|
-
const cwd = payload
|
|
13577
|
+
const cwd = resolveHookCwd(payload);
|
|
13207
13578
|
return JSON.stringify({
|
|
13208
13579
|
session_id: sessionId,
|
|
13209
13580
|
turn_id: turnId,
|
|
@@ -13236,12 +13607,12 @@ async function selfHealHook(repoRoot, tool) {
|
|
|
13236
13607
|
}
|
|
13237
13608
|
}
|
|
13238
13609
|
function resolveCursorTranscriptPath(payload) {
|
|
13239
|
-
const id = payload
|
|
13610
|
+
const id = resolveHookSessionId(payload);
|
|
13240
13611
|
const workspace = payload.workspace_roots?.[0];
|
|
13241
13612
|
if (!id || !workspace) return void 0;
|
|
13242
13613
|
const encoded = workspace.replace(/^\//, "").replace(/\//g, "-");
|
|
13243
|
-
return
|
|
13244
|
-
|
|
13614
|
+
return path15.join(
|
|
13615
|
+
os6.homedir(),
|
|
13245
13616
|
".cursor",
|
|
13246
13617
|
"projects",
|
|
13247
13618
|
encoded,
|
|
@@ -13251,14 +13622,10 @@ function resolveCursorTranscriptPath(payload) {
|
|
|
13251
13622
|
);
|
|
13252
13623
|
}
|
|
13253
13624
|
async function runUploadInner(payload) {
|
|
13254
|
-
const sessionId = payload
|
|
13625
|
+
const sessionId = resolveHookSessionId(payload);
|
|
13255
13626
|
const transcriptPath = payload.transcript_path ?? resolveCursorTranscriptPath(payload);
|
|
13256
|
-
const cwd = payload
|
|
13257
|
-
const
|
|
13258
|
-
appendLog(
|
|
13259
|
-
"info",
|
|
13260
|
-
`[${sessionId ?? "no-id"}] payload parsed (tool=${sourceTool}, cwd=${cwd ?? "?"})`
|
|
13261
|
-
);
|
|
13627
|
+
const cwd = resolveHookCwd(payload);
|
|
13628
|
+
const eventKind = classifyHookEvent(payload.hook_event_name);
|
|
13262
13629
|
if (!sessionId || !transcriptPath || !cwd) {
|
|
13263
13630
|
appendLog(
|
|
13264
13631
|
"warn",
|
|
@@ -13275,10 +13642,20 @@ async function runUploadInner(payload) {
|
|
|
13275
13642
|
return;
|
|
13276
13643
|
}
|
|
13277
13644
|
const { repoRoot, config } = match;
|
|
13645
|
+
const sourceTool = await resolveSourceTool(payload, repoRoot);
|
|
13646
|
+
if (!sourceTool) {
|
|
13647
|
+
payload.tool = "unknown";
|
|
13648
|
+
return;
|
|
13649
|
+
}
|
|
13650
|
+
payload.tool = sourceTool;
|
|
13651
|
+
appendLog(
|
|
13652
|
+
"info",
|
|
13653
|
+
`[${sessionId}] payload parsed (tool=${sourceTool}, cwd=${cwd}, event=${eventKind ?? "?"})`
|
|
13654
|
+
);
|
|
13278
13655
|
await selfHealHook(repoRoot, sourceTool);
|
|
13279
|
-
const transcriptResolved =
|
|
13656
|
+
const transcriptResolved = path15.resolve(transcriptPath);
|
|
13280
13657
|
try {
|
|
13281
|
-
const stat = await
|
|
13658
|
+
const stat = await fs11.promises.stat(transcriptResolved);
|
|
13282
13659
|
if (!stat.isFile()) {
|
|
13283
13660
|
appendLog(
|
|
13284
13661
|
"warn",
|
|
@@ -13305,92 +13682,166 @@ async function runUploadInner(payload) {
|
|
|
13305
13682
|
transcriptPath: transcriptResolved,
|
|
13306
13683
|
repoRoot,
|
|
13307
13684
|
config,
|
|
13308
|
-
sourceTool
|
|
13685
|
+
sourceTool,
|
|
13686
|
+
eventKind
|
|
13309
13687
|
});
|
|
13310
13688
|
}
|
|
13311
13689
|
async function uploadSession(args) {
|
|
13312
|
-
const { sessionId, transcriptPath, repoRoot, config, sourceTool } = args;
|
|
13313
|
-
const
|
|
13314
|
-
|
|
13315
|
-
|
|
13316
|
-
|
|
13317
|
-
|
|
13318
|
-
|
|
13319
|
-
|
|
13320
|
-
|
|
13321
|
-
|
|
13322
|
-
|
|
13323
|
-
|
|
13324
|
-
|
|
13325
|
-
|
|
13326
|
-
|
|
13327
|
-
|
|
13328
|
-
|
|
13329
|
-
|
|
13330
|
-
|
|
13331
|
-
|
|
13332
|
-
|
|
13333
|
-
|
|
13334
|
-
|
|
13335
|
-
|
|
13336
|
-
|
|
13337
|
-
|
|
13338
|
-
|
|
13339
|
-
|
|
13690
|
+
const { sessionId, transcriptPath, repoRoot, config, sourceTool, eventKind } = args;
|
|
13691
|
+
const isSessionEnd = eventKind === "sessionEnd";
|
|
13692
|
+
await withUploadLock(repoRoot, sourceTool, sessionId, async () => {
|
|
13693
|
+
const prior = await readUploadState(repoRoot, sourceTool, sessionId);
|
|
13694
|
+
let transcriptSize;
|
|
13695
|
+
let transcriptSha256;
|
|
13696
|
+
try {
|
|
13697
|
+
transcriptSize = (await fs11.promises.stat(transcriptPath)).size;
|
|
13698
|
+
transcriptSha256 = await hashFileSha256(transcriptPath);
|
|
13699
|
+
} catch {
|
|
13700
|
+
}
|
|
13701
|
+
if (prior?.contributionId && transcriptSize !== void 0 && transcriptSha256 !== void 0 && transcriptSize === prior.lastTranscriptSize && transcriptSha256 === prior.lastTranscriptSha256) {
|
|
13702
|
+
appendLog(
|
|
13703
|
+
"info",
|
|
13704
|
+
`[${sessionId}] skipping ${sourceTool} ${isSessionEnd ? "SessionEnd" : "Stop"} upload (transcript unchanged at ${transcriptSize} bytes${isSessionEnd ? "; local state cleared" : ""})`
|
|
13705
|
+
);
|
|
13706
|
+
if (isSessionEnd) {
|
|
13707
|
+
await deleteUploadState(repoRoot, sourceTool, sessionId);
|
|
13708
|
+
}
|
|
13709
|
+
return;
|
|
13710
|
+
}
|
|
13711
|
+
const now = /* @__PURE__ */ new Date();
|
|
13712
|
+
const sourceFile = {
|
|
13713
|
+
sourceName: sourceTool,
|
|
13714
|
+
absolutePath: transcriptPath,
|
|
13715
|
+
repoPath: repoRoot
|
|
13716
|
+
};
|
|
13717
|
+
const group = {
|
|
13718
|
+
repoPath: repoRoot,
|
|
13719
|
+
label: path15.basename(repoRoot),
|
|
13720
|
+
files: [sourceFile],
|
|
13721
|
+
sourceNames: [sourceTool],
|
|
13722
|
+
lastModified: now
|
|
13723
|
+
};
|
|
13724
|
+
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
13725
|
+
const envFilePaths = envFileNames.map((n) => path15.join(repoRoot, n));
|
|
13726
|
+
const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
|
|
13727
|
+
const mwChain = [];
|
|
13728
|
+
if (secretResult.values.size > 0) {
|
|
13729
|
+
mwChain.push(new RedactMiddleware(secretResult.values));
|
|
13730
|
+
}
|
|
13731
|
+
mwChain.push(new PatternRedactMiddleware());
|
|
13732
|
+
mwChain.push(new NormalizeMiddleware());
|
|
13733
|
+
const identity = await loadIdentity(config.apiBaseUrl);
|
|
13734
|
+
if (!identity) {
|
|
13735
|
+
appendLog(
|
|
13736
|
+
"error",
|
|
13737
|
+
`Session ${sessionId} upload skipped: no saved login for ${config.apiBaseUrl}. Run \`npx hillclimb login\`.`
|
|
13738
|
+
);
|
|
13739
|
+
return;
|
|
13740
|
+
}
|
|
13741
|
+
const client = new PlatformClient(
|
|
13742
|
+
config.apiBaseUrl,
|
|
13743
|
+
identity.sessionCookie
|
|
13340
13744
|
);
|
|
13341
|
-
|
|
13342
|
-
|
|
13343
|
-
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13352
|
-
|
|
13353
|
-
|
|
13354
|
-
const title = `${toolLabel2} session ${shortId} \u2014 ${epochSeconds}`;
|
|
13355
|
-
const body = `Session ID: ${sessionId}
|
|
13745
|
+
const shortId = sessionId.slice(0, 12);
|
|
13746
|
+
const toolLabels = {
|
|
13747
|
+
cursor: "Cursor",
|
|
13748
|
+
codex: "Codex",
|
|
13749
|
+
claude: "Claude",
|
|
13750
|
+
"copilot-chat": "GitHub Copilot Chat",
|
|
13751
|
+
opencode: "opencode"
|
|
13752
|
+
};
|
|
13753
|
+
const toolLabel2 = toolLabels[sourceTool] ?? "Claude";
|
|
13754
|
+
const epochSeconds = formatEpochSeconds2(now);
|
|
13755
|
+
const seq = (prior?.uploadCount ?? 0) + 1;
|
|
13756
|
+
const title = `${toolLabel2} session ${shortId} \u2014 ${epochSeconds}`;
|
|
13757
|
+
const body = `Session ID: ${sessionId}
|
|
13356
13758
|
Tool: ${toolLabel2}
|
|
13357
13759
|
Repo: ${repoRoot}
|
|
13358
13760
|
Uploaded: ${now.toISOString()}`;
|
|
13359
|
-
|
|
13360
|
-
|
|
13361
|
-
|
|
13362
|
-
|
|
13363
|
-
|
|
13364
|
-
|
|
13365
|
-
|
|
13366
|
-
|
|
13367
|
-
|
|
13368
|
-
|
|
13369
|
-
|
|
13370
|
-
|
|
13371
|
-
|
|
13372
|
-
|
|
13761
|
+
const zipFilename = `${sourceTool}-${sanitize2(shortId)}-${epochSeconds}-${String(seq).padStart(3, "0")}.zip`;
|
|
13762
|
+
const alreadySubmitted = prior?.submitted ?? false;
|
|
13763
|
+
const submitThisUpload = config.autoSubmit && !alreadySubmitted;
|
|
13764
|
+
const output = new PlatformUploadOutput({
|
|
13765
|
+
client,
|
|
13766
|
+
projectId: config.projectId,
|
|
13767
|
+
contributionTypeSlug: config.contributionTypeSlug,
|
|
13768
|
+
contributionTitle: title,
|
|
13769
|
+
contributionBody: body,
|
|
13770
|
+
zipFilename,
|
|
13771
|
+
autoSubmit: submitThisUpload,
|
|
13772
|
+
existingContributionId: prior?.contributionId ?? void 0,
|
|
13773
|
+
// Persist the new contribution id before the file PUT so a failed
|
|
13774
|
+
// upload can't make the next Stop create a second contribution.
|
|
13775
|
+
onContributionCreated: (id) => writeUploadState({
|
|
13776
|
+
schemaVersion: CURRENT_SCHEMA_VERSION2,
|
|
13777
|
+
sessionId,
|
|
13778
|
+
tool: sourceTool,
|
|
13779
|
+
repoRoot,
|
|
13780
|
+
projectId: config.projectId,
|
|
13781
|
+
contributionId: id,
|
|
13782
|
+
submitted: alreadySubmitted,
|
|
13783
|
+
uploadCount: prior?.uploadCount ?? 0,
|
|
13784
|
+
firstUploadedAt: prior?.firstUploadedAt ?? now.toISOString(),
|
|
13785
|
+
lastUploadedAt: prior?.lastUploadedAt ?? now.toISOString(),
|
|
13786
|
+
lastTranscriptSize: prior?.lastTranscriptSize,
|
|
13787
|
+
lastTranscriptSha256: prior?.lastTranscriptSha256
|
|
13788
|
+
})
|
|
13373
13789
|
});
|
|
13790
|
+
const reuseDesc = prior?.contributionId ? `reusing contribution ${prior.contributionId} (file #${seq})` : prior ? "new contribution (file #1; prior state had no contribution \u2014 earlier create may have failed)" : "new contribution (file #1; first upload for session)";
|
|
13374
13791
|
appendLog(
|
|
13375
13792
|
"info",
|
|
13376
|
-
`
|
|
13793
|
+
`[${sessionId}] ${sourceTool} ${isSessionEnd ? "SessionEnd" : "Stop"} upload \u2192 ${reuseDesc}`
|
|
13377
13794
|
);
|
|
13378
|
-
|
|
13379
|
-
|
|
13795
|
+
let contributionId;
|
|
13796
|
+
try {
|
|
13797
|
+
contributionId = await runPipeline(group, mwChain, output, {
|
|
13798
|
+
selectedSources: [sourceTool]
|
|
13799
|
+
});
|
|
13800
|
+
} catch (err) {
|
|
13801
|
+
if (err instanceof PlatformError && err.status === 401) {
|
|
13802
|
+
appendLog(
|
|
13803
|
+
"error",
|
|
13804
|
+
`Session ${sessionId} upload failed: authentication expired. Re-run \`npx hillclimb\` in ${repoRoot}.`
|
|
13805
|
+
);
|
|
13806
|
+
return;
|
|
13807
|
+
}
|
|
13380
13808
|
appendLog(
|
|
13381
13809
|
"error",
|
|
13382
|
-
`Session ${sessionId} upload failed:
|
|
13810
|
+
`Session ${sessionId} upload failed: ${err instanceof Error ? err.message : String(err)}`
|
|
13383
13811
|
);
|
|
13384
13812
|
return;
|
|
13385
13813
|
}
|
|
13814
|
+
const next = {
|
|
13815
|
+
schemaVersion: CURRENT_SCHEMA_VERSION2,
|
|
13816
|
+
sessionId,
|
|
13817
|
+
tool: sourceTool,
|
|
13818
|
+
repoRoot,
|
|
13819
|
+
projectId: config.projectId,
|
|
13820
|
+
contributionId,
|
|
13821
|
+
submitted: alreadySubmitted || submitThisUpload,
|
|
13822
|
+
uploadCount: seq,
|
|
13823
|
+
firstUploadedAt: prior?.firstUploadedAt ?? now.toISOString(),
|
|
13824
|
+
lastUploadedAt: now.toISOString(),
|
|
13825
|
+
lastTranscriptSize: transcriptSize,
|
|
13826
|
+
lastTranscriptSha256: transcriptSha256
|
|
13827
|
+
};
|
|
13828
|
+
await writeUploadState(next);
|
|
13386
13829
|
appendLog(
|
|
13387
|
-
"
|
|
13388
|
-
`
|
|
13830
|
+
"info",
|
|
13831
|
+
`Uploaded session ${sessionId} to project ${config.projectSlug} (${config.projectId}) as contribution ${contributionId} (seq=${seq})`
|
|
13389
13832
|
);
|
|
13390
|
-
|
|
13833
|
+
if (isSessionEnd) {
|
|
13834
|
+
await deleteUploadState(repoRoot, sourceTool, sessionId);
|
|
13835
|
+
appendLog(
|
|
13836
|
+
"info",
|
|
13837
|
+
`[${sessionId}] session complete \u2014 contribution ${contributionId}, ${seq} file(s); local state cleared`
|
|
13838
|
+
);
|
|
13839
|
+
}
|
|
13840
|
+
});
|
|
13391
13841
|
}
|
|
13392
13842
|
var WORKER_ENV_FLAG = "HILLCLIMB_UPLOAD_WORKER";
|
|
13393
13843
|
var TOOL_ENV_FLAG = "HILLCLIMB_UPLOAD_TOOL";
|
|
13844
|
+
var FLOW_ID_ENV = "HILLCLIMB_UPLOAD_FLOW";
|
|
13394
13845
|
function parseToolArg(argv) {
|
|
13395
13846
|
for (let i = 0; i < argv.length; i++) {
|
|
13396
13847
|
const a = argv[i];
|
|
@@ -13404,6 +13855,8 @@ async function runUpload() {
|
|
|
13404
13855
|
await runUploadWorker();
|
|
13405
13856
|
return;
|
|
13406
13857
|
}
|
|
13858
|
+
const flowId = newFlowId();
|
|
13859
|
+
setLogPrefix(`[${flowId}]`);
|
|
13407
13860
|
appendLog("info", `upload hook invoked (pid ${process.pid})`);
|
|
13408
13861
|
let raw;
|
|
13409
13862
|
try {
|
|
@@ -13430,7 +13883,8 @@ async function runUpload() {
|
|
|
13430
13883
|
const toolArg = parseToolArg(process.argv.slice(2));
|
|
13431
13884
|
const workerEnv = {
|
|
13432
13885
|
...process.env,
|
|
13433
|
-
[WORKER_ENV_FLAG]: "1"
|
|
13886
|
+
[WORKER_ENV_FLAG]: "1",
|
|
13887
|
+
[FLOW_ID_ENV]: flowId
|
|
13434
13888
|
};
|
|
13435
13889
|
if (toolArg) workerEnv[TOOL_ENV_FLAG] = toolArg;
|
|
13436
13890
|
try {
|
|
@@ -13457,6 +13911,7 @@ async function runUpload() {
|
|
|
13457
13911
|
}
|
|
13458
13912
|
}
|
|
13459
13913
|
async function runUploadWorker() {
|
|
13914
|
+
setLogPrefix(`[${process.env[FLOW_ID_ENV] ?? newFlowId()}]`);
|
|
13460
13915
|
appendLog("info", `upload worker started (pid ${process.pid})`);
|
|
13461
13916
|
let raw;
|
|
13462
13917
|
try {
|
|
@@ -13495,25 +13950,29 @@ async function runUploadWorker() {
|
|
|
13495
13950
|
} finally {
|
|
13496
13951
|
await recordDebugLogCompletion({
|
|
13497
13952
|
kind: "agent",
|
|
13498
|
-
tool:
|
|
13953
|
+
tool: fallbackSourceTool(payload),
|
|
13499
13954
|
payload
|
|
13500
13955
|
});
|
|
13956
|
+
try {
|
|
13957
|
+
await sweepStaleUploadStates();
|
|
13958
|
+
} catch {
|
|
13959
|
+
}
|
|
13501
13960
|
}
|
|
13502
13961
|
}
|
|
13503
13962
|
|
|
13504
13963
|
// src/git-traces/index.ts
|
|
13505
13964
|
import { spawn as spawn3 } from "child_process";
|
|
13506
|
-
import
|
|
13965
|
+
import crypto5 from "crypto";
|
|
13507
13966
|
|
|
13508
13967
|
// src/git-traces/handlers.ts
|
|
13509
|
-
import { execFileSync as
|
|
13510
|
-
import
|
|
13968
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
13969
|
+
import path18 from "path";
|
|
13511
13970
|
|
|
13512
13971
|
// src/git-traces/git-ops.ts
|
|
13513
|
-
import { execFileSync } from "child_process";
|
|
13514
|
-
import
|
|
13515
|
-
import
|
|
13516
|
-
import
|
|
13972
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
13973
|
+
import fs12 from "fs";
|
|
13974
|
+
import os7 from "os";
|
|
13975
|
+
import path16 from "path";
|
|
13517
13976
|
import { gzipSync } from "zlib";
|
|
13518
13977
|
var GIT_COMMAND_TIMEOUT_MS = 12e4;
|
|
13519
13978
|
var EXEC_OPTS = {
|
|
@@ -13567,7 +14026,7 @@ function isGitTimeoutError(err) {
|
|
|
13567
14026
|
}
|
|
13568
14027
|
function gitBuffer(repoRoot, args, options = {}) {
|
|
13569
14028
|
try {
|
|
13570
|
-
return
|
|
14029
|
+
return execFileSync2("git", args, {
|
|
13571
14030
|
cwd: repoRoot,
|
|
13572
14031
|
...options.env ? { env: options.env } : {},
|
|
13573
14032
|
...options.input !== void 0 ? { input: options.input } : {},
|
|
@@ -13676,8 +14135,8 @@ function removePathsFromIndex(repoRoot, env, paths) {
|
|
|
13676
14135
|
function filterOversizedFilesFromTree(repoRoot, treeSha, options = {}) {
|
|
13677
14136
|
const oversizedFiles = listOversizedTreeFiles(repoRoot, treeSha, options);
|
|
13678
14137
|
if (oversizedFiles.length === 0) return treeSha;
|
|
13679
|
-
const tmpIndex =
|
|
13680
|
-
|
|
14138
|
+
const tmpIndex = path16.join(
|
|
14139
|
+
os7.tmpdir(),
|
|
13681
14140
|
`hillclimb-filter-${Date.now()}-${process.pid}`
|
|
13682
14141
|
);
|
|
13683
14142
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -13691,7 +14150,7 @@ function filterOversizedFilesFromTree(repoRoot, treeSha, options = {}) {
|
|
|
13691
14150
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13692
14151
|
} finally {
|
|
13693
14152
|
try {
|
|
13694
|
-
|
|
14153
|
+
fs12.unlinkSync(tmpIndex);
|
|
13695
14154
|
} catch {
|
|
13696
14155
|
}
|
|
13697
14156
|
}
|
|
@@ -13708,7 +14167,7 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13708
14167
|
for (const relPath of list.split("\0")) {
|
|
13709
14168
|
if (!relPath) continue;
|
|
13710
14169
|
try {
|
|
13711
|
-
const stat =
|
|
14170
|
+
const stat = fs12.lstatSync(path16.join(repoRoot, relPath));
|
|
13712
14171
|
if (stat.size > MAX_SNAPSHOT_FILE_BYTES) {
|
|
13713
14172
|
recordOmittedSnapshotFile(omittedFiles, {
|
|
13714
14173
|
path: relPath,
|
|
@@ -13723,8 +14182,8 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13723
14182
|
}
|
|
13724
14183
|
}
|
|
13725
14184
|
if (kept.length === 0) return null;
|
|
13726
|
-
const tmpIndex =
|
|
13727
|
-
|
|
14185
|
+
const tmpIndex = path16.join(
|
|
14186
|
+
os7.tmpdir(),
|
|
13728
14187
|
`hillclimb-untracked-${Date.now()}-${process.pid}`
|
|
13729
14188
|
);
|
|
13730
14189
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -13736,7 +14195,7 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13736
14195
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13737
14196
|
} finally {
|
|
13738
14197
|
try {
|
|
13739
|
-
|
|
14198
|
+
fs12.unlinkSync(tmpIndex);
|
|
13740
14199
|
} catch {
|
|
13741
14200
|
}
|
|
13742
14201
|
}
|
|
@@ -13753,8 +14212,8 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
|
13753
14212
|
const untrackedTree = buildUntrackedTree(repoRoot, options.omittedFiles);
|
|
13754
14213
|
if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA)
|
|
13755
14214
|
return filteredTrackedTree;
|
|
13756
|
-
const tmpIndex =
|
|
13757
|
-
|
|
14215
|
+
const tmpIndex = path16.join(
|
|
14216
|
+
os7.tmpdir(),
|
|
13758
14217
|
`hillclimb-index-${Date.now()}-${process.pid}`
|
|
13759
14218
|
);
|
|
13760
14219
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -13781,7 +14240,7 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
|
13781
14240
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13782
14241
|
} finally {
|
|
13783
14242
|
try {
|
|
13784
|
-
|
|
14243
|
+
fs12.unlinkSync(tmpIndex);
|
|
13785
14244
|
} catch {
|
|
13786
14245
|
}
|
|
13787
14246
|
}
|
|
@@ -13795,16 +14254,19 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
|
|
|
13795
14254
|
]);
|
|
13796
14255
|
const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
|
|
13797
14256
|
pinRef(repoRoot, orphanRef, orphanCommit);
|
|
13798
|
-
const tmpFile =
|
|
13799
|
-
|
|
13800
|
-
|
|
14257
|
+
const tmpFile = path16.join(
|
|
14258
|
+
os7.tmpdir(),
|
|
14259
|
+
// Include the pid (like the other temp files in this module) so concurrent
|
|
14260
|
+
// git-traces workers — e.g. two sessions, or a parent + subagent — don't
|
|
14261
|
+
// collide on the same `git bundle create` path and its `.lock`.
|
|
14262
|
+
`hillclimb-bundle-${Date.now()}-${process.pid}.bundle`
|
|
13801
14263
|
);
|
|
13802
14264
|
try {
|
|
13803
14265
|
git(repoRoot, ["bundle", "create", tmpFile, orphanRef]);
|
|
13804
|
-
return
|
|
14266
|
+
return fs12.readFileSync(tmpFile);
|
|
13805
14267
|
} finally {
|
|
13806
14268
|
try {
|
|
13807
|
-
|
|
14269
|
+
fs12.unlinkSync(tmpFile);
|
|
13808
14270
|
} catch {
|
|
13809
14271
|
}
|
|
13810
14272
|
deleteRef(repoRoot, orphanRef);
|
|
@@ -13902,7 +14364,7 @@ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersio
|
|
|
13902
14364
|
...omittedFiles.length > 0 ? { omittedFiles } : {}
|
|
13903
14365
|
},
|
|
13904
14366
|
author: { name: authorName, email: authorEmail },
|
|
13905
|
-
hostname:
|
|
14367
|
+
hostname: os7.hostname(),
|
|
13906
14368
|
cliVersion,
|
|
13907
14369
|
commits
|
|
13908
14370
|
};
|
|
@@ -13991,9 +14453,9 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
13991
14453
|
oldPath
|
|
13992
14454
|
});
|
|
13993
14455
|
} else {
|
|
13994
|
-
const
|
|
13995
|
-
indexByPath.set(
|
|
13996
|
-
files.push({ path:
|
|
14456
|
+
const path25 = parts[parts.length - 1];
|
|
14457
|
+
indexByPath.set(path25, files.length);
|
|
14458
|
+
files.push({ path: path25, status, additions: 0, deletions: 0 });
|
|
13997
14459
|
}
|
|
13998
14460
|
}
|
|
13999
14461
|
for (const line of numstat.split("\n")) {
|
|
@@ -14059,31 +14521,31 @@ function cleanupSessionRefs(repoRoot, sessionId) {
|
|
|
14059
14521
|
}
|
|
14060
14522
|
|
|
14061
14523
|
// src/git-traces/session-state.ts
|
|
14062
|
-
import
|
|
14063
|
-
import
|
|
14064
|
-
import
|
|
14065
|
-
import
|
|
14066
|
-
var
|
|
14067
|
-
var
|
|
14068
|
-
var
|
|
14069
|
-
var
|
|
14070
|
-
function
|
|
14071
|
-
return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ??
|
|
14524
|
+
import crypto4 from "crypto";
|
|
14525
|
+
import fs13 from "fs";
|
|
14526
|
+
import os8 from "os";
|
|
14527
|
+
import path17 from "path";
|
|
14528
|
+
var CURRENT_SCHEMA_VERSION3 = 3;
|
|
14529
|
+
var DEFAULT_STATE_DIR2 = path17.join(os8.homedir(), ".hillclimb", "git-traces");
|
|
14530
|
+
var LOCK_RETRIES3 = 120;
|
|
14531
|
+
var LOCK_RETRY_DELAY_MS3 = 500;
|
|
14532
|
+
function stateDir3() {
|
|
14533
|
+
return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR2;
|
|
14072
14534
|
}
|
|
14073
14535
|
function stateFileForRepo(repoRoot, tool, sessionId) {
|
|
14074
|
-
const hash =
|
|
14075
|
-
sessionId ? `${
|
|
14536
|
+
const hash = crypto4.createHash("sha256").update(
|
|
14537
|
+
sessionId ? `${path17.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path17.resolve(repoRoot)}\0${tool}`
|
|
14076
14538
|
).digest("hex").slice(0, 16);
|
|
14077
|
-
return
|
|
14539
|
+
return path17.join(stateDir3(), `${hash}.json`);
|
|
14078
14540
|
}
|
|
14079
14541
|
function lockFileForRepo(repoRoot, tool) {
|
|
14080
14542
|
return `${stateFileForRepo(repoRoot, tool)}.lock`;
|
|
14081
14543
|
}
|
|
14082
14544
|
async function readStateFile(file) {
|
|
14083
14545
|
try {
|
|
14084
|
-
const raw = await
|
|
14546
|
+
const raw = await fs13.promises.readFile(file, "utf-8");
|
|
14085
14547
|
const parsed = JSON.parse(raw);
|
|
14086
|
-
if (parsed.schemaVersion !==
|
|
14548
|
+
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION3) {
|
|
14087
14549
|
return null;
|
|
14088
14550
|
}
|
|
14089
14551
|
return parsed;
|
|
@@ -14094,26 +14556,26 @@ async function readStateFile(file) {
|
|
|
14094
14556
|
async function listScopedSessionStates(repoRoot, tool) {
|
|
14095
14557
|
let entries;
|
|
14096
14558
|
try {
|
|
14097
|
-
entries = await
|
|
14559
|
+
entries = await fs13.promises.readdir(stateDir3(), { withFileTypes: true });
|
|
14098
14560
|
} catch {
|
|
14099
14561
|
return [];
|
|
14100
14562
|
}
|
|
14101
14563
|
const states = [];
|
|
14102
14564
|
for (const entry of entries) {
|
|
14103
14565
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
14104
|
-
const file =
|
|
14566
|
+
const file = path17.join(stateDir3(), entry.name);
|
|
14105
14567
|
const state = await readStateFile(file);
|
|
14106
14568
|
if (!state) continue;
|
|
14107
14569
|
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
14108
14570
|
continue;
|
|
14109
14571
|
}
|
|
14110
|
-
if (
|
|
14111
|
-
if (
|
|
14572
|
+
if (path17.resolve(state.repoRoot) !== path17.resolve(repoRoot)) continue;
|
|
14573
|
+
if (path17.resolve(file) !== path17.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
|
|
14112
14574
|
continue;
|
|
14113
14575
|
}
|
|
14114
14576
|
let mtimeMs = 0;
|
|
14115
14577
|
try {
|
|
14116
|
-
mtimeMs = (await
|
|
14578
|
+
mtimeMs = (await fs13.promises.stat(file)).mtimeMs;
|
|
14117
14579
|
} catch {
|
|
14118
14580
|
continue;
|
|
14119
14581
|
}
|
|
@@ -14124,26 +14586,26 @@ async function listScopedSessionStates(repoRoot, tool) {
|
|
|
14124
14586
|
async function listSessionStatesForSession(tool, sessionId) {
|
|
14125
14587
|
let entries;
|
|
14126
14588
|
try {
|
|
14127
|
-
entries = await
|
|
14589
|
+
entries = await fs13.promises.readdir(stateDir3(), { withFileTypes: true });
|
|
14128
14590
|
} catch {
|
|
14129
14591
|
return [];
|
|
14130
14592
|
}
|
|
14131
14593
|
const states = [];
|
|
14132
14594
|
for (const entry of entries) {
|
|
14133
14595
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
14134
|
-
const file =
|
|
14596
|
+
const file = path17.join(stateDir3(), entry.name);
|
|
14135
14597
|
const state = await readStateFile(file);
|
|
14136
14598
|
if (!state) continue;
|
|
14137
14599
|
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
14138
14600
|
continue;
|
|
14139
14601
|
}
|
|
14140
14602
|
if (state.sessionId !== sessionId) continue;
|
|
14141
|
-
if (
|
|
14603
|
+
if (path17.resolve(file) !== path17.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
|
|
14142
14604
|
continue;
|
|
14143
14605
|
}
|
|
14144
14606
|
let mtimeMs = 0;
|
|
14145
14607
|
try {
|
|
14146
|
-
mtimeMs = (await
|
|
14608
|
+
mtimeMs = (await fs13.promises.stat(file)).mtimeMs;
|
|
14147
14609
|
} catch {
|
|
14148
14610
|
continue;
|
|
14149
14611
|
}
|
|
@@ -14164,12 +14626,12 @@ async function readSessionState(repoRoot, tool, sessionId) {
|
|
|
14164
14626
|
}
|
|
14165
14627
|
async function writeSessionState(state, tool) {
|
|
14166
14628
|
const file = stateFileForRepo(state.repoRoot, tool, state.sessionId);
|
|
14167
|
-
await
|
|
14629
|
+
await fs13.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
|
|
14168
14630
|
const tmp = `${file}.tmp`;
|
|
14169
|
-
await
|
|
14631
|
+
await fs13.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
14170
14632
|
mode: 384
|
|
14171
14633
|
});
|
|
14172
|
-
await
|
|
14634
|
+
await fs13.promises.rename(tmp, file);
|
|
14173
14635
|
const legacyFile = stateFileForRepo(state.repoRoot, tool);
|
|
14174
14636
|
const legacy = await readStateFile(legacyFile);
|
|
14175
14637
|
if (legacy?.sessionId === state.sessionId) {
|
|
@@ -14178,7 +14640,7 @@ async function writeSessionState(state, tool) {
|
|
|
14178
14640
|
}
|
|
14179
14641
|
async function deleteStateFile(file) {
|
|
14180
14642
|
try {
|
|
14181
|
-
await
|
|
14643
|
+
await fs13.promises.unlink(file);
|
|
14182
14644
|
} catch {
|
|
14183
14645
|
}
|
|
14184
14646
|
}
|
|
@@ -14194,14 +14656,14 @@ async function deleteSessionState(repoRoot, tool, sessionId) {
|
|
|
14194
14656
|
}
|
|
14195
14657
|
await deleteStateFile(stateFileForRepo(repoRoot, tool));
|
|
14196
14658
|
}
|
|
14197
|
-
async function
|
|
14659
|
+
async function acquireLock3(repoRoot, tool, retries = LOCK_RETRIES3, delayMs = LOCK_RETRY_DELAY_MS3) {
|
|
14198
14660
|
const lockPath = lockFileForRepo(repoRoot, tool);
|
|
14199
|
-
await
|
|
14661
|
+
await fs13.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
|
|
14200
14662
|
for (let i = 0; i < retries; i++) {
|
|
14201
14663
|
try {
|
|
14202
|
-
const fd = await
|
|
14664
|
+
const fd = await fs13.promises.open(
|
|
14203
14665
|
lockPath,
|
|
14204
|
-
|
|
14666
|
+
fs13.constants.O_CREAT | fs13.constants.O_EXCL | fs13.constants.O_WRONLY
|
|
14205
14667
|
);
|
|
14206
14668
|
await fd.write(String(process.pid));
|
|
14207
14669
|
await fd.close();
|
|
@@ -14216,9 +14678,9 @@ async function acquireLock2(repoRoot, tool, retries = LOCK_RETRIES2, delayMs = L
|
|
|
14216
14678
|
}
|
|
14217
14679
|
throw new Error(`Failed to acquire lock after ${retries} retries`);
|
|
14218
14680
|
}
|
|
14219
|
-
async function
|
|
14681
|
+
async function releaseLock3(repoRoot, tool) {
|
|
14220
14682
|
try {
|
|
14221
|
-
await
|
|
14683
|
+
await fs13.promises.unlink(lockFileForRepo(repoRoot, tool));
|
|
14222
14684
|
} catch {
|
|
14223
14685
|
}
|
|
14224
14686
|
}
|
|
@@ -14241,18 +14703,18 @@ var TOOL_LABELS = {
|
|
|
14241
14703
|
async function loadConfiguredRepos() {
|
|
14242
14704
|
const file = await loadProjects();
|
|
14243
14705
|
return Object.entries(file.projects).map(([repoRoot, config]) => ({
|
|
14244
|
-
repoRoot:
|
|
14706
|
+
repoRoot: path18.resolve(repoRoot),
|
|
14245
14707
|
config
|
|
14246
14708
|
})).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
|
|
14247
14709
|
}
|
|
14248
14710
|
function repoLabel(repoRoot) {
|
|
14249
|
-
return
|
|
14711
|
+
return path18.basename(repoRoot) || repoRoot;
|
|
14250
14712
|
}
|
|
14251
14713
|
function resolveCwd2(payload) {
|
|
14252
|
-
return payload
|
|
14714
|
+
return resolveHookCwd(payload);
|
|
14253
14715
|
}
|
|
14254
14716
|
function resolveSessionId2(payload) {
|
|
14255
|
-
return payload
|
|
14717
|
+
return resolveHookSessionId(payload);
|
|
14256
14718
|
}
|
|
14257
14719
|
function epochPrefix(epoch) {
|
|
14258
14720
|
return `epoch-${String(epoch).padStart(3, "0")}`;
|
|
@@ -14268,7 +14730,7 @@ function captureHeadSha(cwd) {
|
|
|
14268
14730
|
}
|
|
14269
14731
|
}
|
|
14270
14732
|
function execGit(cwd, args) {
|
|
14271
|
-
return
|
|
14733
|
+
return execFileSync3("git", args, {
|
|
14272
14734
|
cwd,
|
|
14273
14735
|
stdio: ["pipe", "pipe", "pipe"],
|
|
14274
14736
|
timeout: 3e4,
|
|
@@ -14461,27 +14923,34 @@ async function createGitTracesContribution(params) {
|
|
|
14461
14923
|
const epochSeconds = formatEpochSeconds3(now);
|
|
14462
14924
|
const shortId = state.sessionId.slice(0, 12);
|
|
14463
14925
|
const repoName = repoLabel(repoRoot);
|
|
14464
|
-
|
|
14465
|
-
|
|
14466
|
-
|
|
14467
|
-
|
|
14926
|
+
let contributionId = state.contributionId;
|
|
14927
|
+
if (!contributionId) {
|
|
14928
|
+
const contribution = await client.createContribution(config.projectId, {
|
|
14929
|
+
contributionTypeSlug: GIT_TRACES_SLUG,
|
|
14930
|
+
title: `${toolLabel2} session ${shortId} \u2014 ${repoName} \u2014 ${epochSeconds}`,
|
|
14931
|
+
body: `Session ID: ${state.sessionId}
|
|
14468
14932
|
Tool: ${toolLabel2}
|
|
14469
14933
|
Repo: ${repoRoot}
|
|
14470
14934
|
Uploaded: ${now.toISOString()}`
|
|
14471
|
-
|
|
14935
|
+
});
|
|
14936
|
+
contributionId = contribution.id;
|
|
14937
|
+
state.contributionId = contributionId;
|
|
14938
|
+
state.baselineUploaded = false;
|
|
14939
|
+
await writeSessionState(state, tool);
|
|
14940
|
+
}
|
|
14472
14941
|
const uploaded = await uploadEpochBaselineArtifacts({
|
|
14473
14942
|
client,
|
|
14474
|
-
contributionId
|
|
14943
|
+
contributionId,
|
|
14475
14944
|
epoch: 1,
|
|
14476
14945
|
artifacts
|
|
14477
14946
|
});
|
|
14478
14947
|
if (!uploaded) return null;
|
|
14479
|
-
await client.submitContribution(
|
|
14948
|
+
await client.submitContribution(contributionId);
|
|
14480
14949
|
appendLog(
|
|
14481
14950
|
"info",
|
|
14482
|
-
`git-traces: baseline uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${
|
|
14951
|
+
`git-traces: baseline uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId}, epoch=1, bundleBytes=${artifacts.bundleBuffer.byteLength}, metadataBytes=${artifacts.metadataBuffer.byteLength})`
|
|
14483
14952
|
);
|
|
14484
|
-
return
|
|
14953
|
+
return contributionId;
|
|
14485
14954
|
}
|
|
14486
14955
|
async function uploadEpochBaseline(params) {
|
|
14487
14956
|
const artifacts = buildEpochBaselineArtifacts(params);
|
|
@@ -14519,9 +14988,10 @@ async function initializeSession(repoRoot, tool, sessionId) {
|
|
|
14519
14988
|
});
|
|
14520
14989
|
if (!frozen) return null;
|
|
14521
14990
|
const state = {
|
|
14522
|
-
schemaVersion:
|
|
14991
|
+
schemaVersion: CURRENT_SCHEMA_VERSION3,
|
|
14523
14992
|
sessionId,
|
|
14524
14993
|
contributionId: null,
|
|
14994
|
+
baselineUploaded: false,
|
|
14525
14995
|
baselineSha: frozen.baselineSha,
|
|
14526
14996
|
baselineTreeSha: frozen.baselineTreeSha,
|
|
14527
14997
|
baselineMetadata: frozen.baselineMetadata,
|
|
@@ -14569,7 +15039,7 @@ async function processSessionStartRepo(repo, tool, sessionId) {
|
|
|
14569
15039
|
);
|
|
14570
15040
|
return "skipped";
|
|
14571
15041
|
}
|
|
14572
|
-
await
|
|
15042
|
+
await acquireLock3(repoRoot, tool);
|
|
14573
15043
|
try {
|
|
14574
15044
|
const staleLegacy = await readSessionState(repoRoot, tool);
|
|
14575
15045
|
if (staleLegacy && staleLegacy.sessionId !== sessionId) {
|
|
@@ -14600,7 +15070,7 @@ async function processSessionStartRepo(repo, tool, sessionId) {
|
|
|
14600
15070
|
);
|
|
14601
15071
|
return "failed";
|
|
14602
15072
|
} finally {
|
|
14603
|
-
await
|
|
15073
|
+
await releaseLock3(repoRoot, tool);
|
|
14604
15074
|
}
|
|
14605
15075
|
}
|
|
14606
15076
|
async function handleSessionStart(payload, tool) {
|
|
@@ -14682,6 +15152,7 @@ async function registerInitialContribution(params) {
|
|
|
14682
15152
|
});
|
|
14683
15153
|
if (!contributionId) return false;
|
|
14684
15154
|
state.contributionId = contributionId;
|
|
15155
|
+
state.baselineUploaded = true;
|
|
14685
15156
|
state.baselineTreeSha = artifacts.baselineTreeSha;
|
|
14686
15157
|
state.lastSnapshotTreeSha = artifacts.baselineTreeSha;
|
|
14687
15158
|
await writeSessionState(state, tool);
|
|
@@ -14700,7 +15171,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14700
15171
|
);
|
|
14701
15172
|
return "skipped";
|
|
14702
15173
|
}
|
|
14703
|
-
await
|
|
15174
|
+
await acquireLock3(repoRoot, tool);
|
|
14704
15175
|
let state = null;
|
|
14705
15176
|
try {
|
|
14706
15177
|
state = await readSessionState(repoRoot, tool, sessionId);
|
|
@@ -14723,7 +15194,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14723
15194
|
if (currentHeadSha !== state.headSha) {
|
|
14724
15195
|
const client2 = await loadRepoClient(repo);
|
|
14725
15196
|
if (!client2) return "skipped";
|
|
14726
|
-
if (state.contributionId === null) {
|
|
15197
|
+
if (state.contributionId === null || !state.baselineUploaded) {
|
|
14727
15198
|
const artifacts2 = buildInitialBaselineArtifactsForState(
|
|
14728
15199
|
repoRoot,
|
|
14729
15200
|
tool,
|
|
@@ -14806,7 +15277,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14806
15277
|
const nextTurnCount = state.turnCount + 1;
|
|
14807
15278
|
const turnLabel = turnSuffix(nextTurnCount);
|
|
14808
15279
|
const filename = `${prefix}-${turnLabel}-${recordedAt}.patch.gz`;
|
|
14809
|
-
if (state.contributionId === null && !canUploadFile(filename, patchBuffer)) {
|
|
15280
|
+
if ((state.contributionId === null || !state.baselineUploaded) && !canUploadFile(filename, patchBuffer)) {
|
|
14810
15281
|
appendLog(
|
|
14811
15282
|
"warn",
|
|
14812
15283
|
`git-traces: first changed turn skipped before contribution creation (repo=${repoRoot}, project=${config.projectId}, reason=patch-too-large)`
|
|
@@ -14815,7 +15286,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14815
15286
|
}
|
|
14816
15287
|
const client = await loadRepoClient(repo);
|
|
14817
15288
|
if (!client) return "skipped";
|
|
14818
|
-
if (state.contributionId === null) {
|
|
15289
|
+
if (state.contributionId === null || !state.baselineUploaded) {
|
|
14819
15290
|
const artifacts = buildInitialBaselineArtifactsForState(
|
|
14820
15291
|
repoRoot,
|
|
14821
15292
|
tool,
|
|
@@ -14880,6 +15351,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14880
15351
|
{
|
|
14881
15352
|
...state,
|
|
14882
15353
|
contributionId: null,
|
|
15354
|
+
baselineUploaded: false,
|
|
14883
15355
|
lastSnapshotSha: state.baselineSha,
|
|
14884
15356
|
lastSnapshotTreeSha: state.baselineTreeSha,
|
|
14885
15357
|
turnCount: 0
|
|
@@ -14901,7 +15373,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14901
15373
|
}
|
|
14902
15374
|
return "failed";
|
|
14903
15375
|
} finally {
|
|
14904
|
-
await
|
|
15376
|
+
await releaseLock3(repoRoot, tool);
|
|
14905
15377
|
}
|
|
14906
15378
|
}
|
|
14907
15379
|
async function handleStop(payload, tool) {
|
|
@@ -14918,7 +15390,7 @@ async function handleStop(payload, tool) {
|
|
|
14918
15390
|
if (sessionId) {
|
|
14919
15391
|
const storedStates = await listSessionStatesForSession(tool, sessionId);
|
|
14920
15392
|
for (const { state } of storedStates) {
|
|
14921
|
-
const repo = repoByRoot.get(
|
|
15393
|
+
const repo = repoByRoot.get(path18.resolve(state.repoRoot));
|
|
14922
15394
|
if (!repo) {
|
|
14923
15395
|
missingConfig++;
|
|
14924
15396
|
appendLog(
|
|
@@ -14954,7 +15426,7 @@ async function handleStop(payload, tool) {
|
|
|
14954
15426
|
);
|
|
14955
15427
|
}
|
|
14956
15428
|
async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
|
|
14957
|
-
await
|
|
15429
|
+
await acquireLock3(repoRoot, tool);
|
|
14958
15430
|
try {
|
|
14959
15431
|
const state = await readSessionState(repoRoot, tool, sessionId);
|
|
14960
15432
|
if (!state) return "no-state";
|
|
@@ -14972,7 +15444,7 @@ async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
|
|
|
14972
15444
|
);
|
|
14973
15445
|
return "failed";
|
|
14974
15446
|
} finally {
|
|
14975
|
-
await
|
|
15447
|
+
await releaseLock3(repoRoot, tool);
|
|
14976
15448
|
}
|
|
14977
15449
|
}
|
|
14978
15450
|
async function handleSessionEnd(payload, tool) {
|
|
@@ -14980,21 +15452,62 @@ async function handleSessionEnd(payload, tool) {
|
|
|
14980
15452
|
const sessionId = resolveSessionId2(payload);
|
|
14981
15453
|
const project = cwd ? await findProjectForCwd(cwd) : null;
|
|
14982
15454
|
const triggerRepo = project?.repoRoot ?? cwd ?? "<none>";
|
|
15455
|
+
const recordedAt = Date.now();
|
|
14983
15456
|
const repoRoots = [];
|
|
14984
15457
|
if (sessionId) {
|
|
14985
15458
|
const states = await listSessionStatesForSession(tool, sessionId);
|
|
14986
15459
|
for (const { state } of states) {
|
|
14987
|
-
repoRoots.push(
|
|
15460
|
+
repoRoots.push(path18.resolve(state.repoRoot));
|
|
14988
15461
|
}
|
|
14989
15462
|
}
|
|
14990
15463
|
if (repoRoots.length === 0 && cwd) {
|
|
14991
15464
|
repoRoots.push(project?.repoRoot ?? cwd);
|
|
14992
15465
|
}
|
|
14993
15466
|
if (repoRoots.length === 0) return;
|
|
15467
|
+
const repos = await loadConfiguredRepos();
|
|
15468
|
+
const repoByRoot = new Map(repos.map((repo) => [repo.repoRoot, repo]));
|
|
14994
15469
|
let cleaned = 0;
|
|
14995
15470
|
let noState = 0;
|
|
15471
|
+
let uploaded = 0;
|
|
15472
|
+
let unchanged = 0;
|
|
15473
|
+
let skipped = 0;
|
|
14996
15474
|
let failed = 0;
|
|
14997
15475
|
for (const repoRoot of repoRoots) {
|
|
15476
|
+
const repo = repoByRoot.get(path18.resolve(repoRoot)) ?? (project && path18.resolve(project.repoRoot) === path18.resolve(repoRoot) ? { repoRoot: project.repoRoot, config: project.config } : null);
|
|
15477
|
+
if (!repo) {
|
|
15478
|
+
skipped++;
|
|
15479
|
+
appendLog(
|
|
15480
|
+
"warn",
|
|
15481
|
+
`git-traces: preserving SessionEnd state for repo ${repoRoot} (reason=missing-config)`
|
|
15482
|
+
);
|
|
15483
|
+
continue;
|
|
15484
|
+
}
|
|
15485
|
+
const finalOutcome = await processStopRepo(
|
|
15486
|
+
repo,
|
|
15487
|
+
tool,
|
|
15488
|
+
sessionId,
|
|
15489
|
+
recordedAt
|
|
15490
|
+
);
|
|
15491
|
+
if (finalOutcome === "uploaded") uploaded++;
|
|
15492
|
+
else if (finalOutcome === "unchanged") unchanged++;
|
|
15493
|
+
else if (finalOutcome === "skipped") {
|
|
15494
|
+
skipped++;
|
|
15495
|
+
appendLog(
|
|
15496
|
+
"warn",
|
|
15497
|
+
`git-traces: preserving SessionEnd state for repo ${repoRoot} after skipped final upload`
|
|
15498
|
+
);
|
|
15499
|
+
continue;
|
|
15500
|
+
} else if (finalOutcome === "failed") {
|
|
15501
|
+
failed++;
|
|
15502
|
+
appendLog(
|
|
15503
|
+
"warn",
|
|
15504
|
+
`git-traces: preserving SessionEnd state for repo ${repoRoot} after failed final upload`
|
|
15505
|
+
);
|
|
15506
|
+
continue;
|
|
15507
|
+
} else {
|
|
15508
|
+
noState++;
|
|
15509
|
+
continue;
|
|
15510
|
+
}
|
|
14998
15511
|
const outcome = await cleanupSessionStateForRepo(repoRoot, tool, sessionId);
|
|
14999
15512
|
if (outcome === "cleaned") cleaned++;
|
|
15000
15513
|
else if (outcome === "no-state") noState++;
|
|
@@ -15002,16 +15515,16 @@ async function handleSessionEnd(payload, tool) {
|
|
|
15002
15515
|
}
|
|
15003
15516
|
appendLog(
|
|
15004
15517
|
"info",
|
|
15005
|
-
`git-traces: SessionEnd summary (session=${sessionId ?? "<none>"}, tool=${tool}, triggerRepo=${triggerRepo}, states=${repoRoots.length}, cleaned=${cleaned}, noState=${noState}, failed=${failed})`
|
|
15518
|
+
`git-traces: SessionEnd summary (session=${sessionId ?? "<none>"}, tool=${tool}, triggerRepo=${triggerRepo}, states=${repoRoots.length}, uploaded=${uploaded}, unchanged=${unchanged}, skipped=${skipped}, cleaned=${cleaned}, noState=${noState}, failed=${failed})`
|
|
15006
15519
|
);
|
|
15007
15520
|
}
|
|
15008
15521
|
|
|
15009
15522
|
// src/git-traces/index.ts
|
|
15010
15523
|
var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
|
|
15011
15524
|
var TOOL_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_TOOL";
|
|
15012
|
-
var
|
|
15013
|
-
function
|
|
15014
|
-
return
|
|
15525
|
+
var FLOW_ID_ENV2 = "HILLCLIMB_GIT_TRACES_FLOW";
|
|
15526
|
+
function newFlowId2() {
|
|
15527
|
+
return crypto5.randomBytes(3).toString("hex");
|
|
15015
15528
|
}
|
|
15016
15529
|
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
15017
15530
|
"claude",
|
|
@@ -15150,7 +15663,7 @@ async function runGitTraces() {
|
|
|
15150
15663
|
await runGitTracesWorker();
|
|
15151
15664
|
return;
|
|
15152
15665
|
}
|
|
15153
|
-
const flowId =
|
|
15666
|
+
const flowId = newFlowId2();
|
|
15154
15667
|
setLogPrefix(`[${flowId}]`);
|
|
15155
15668
|
const toolArg = parseToolArg2(process.argv.slice(2));
|
|
15156
15669
|
appendLog(
|
|
@@ -15193,7 +15706,7 @@ async function runGitTraces() {
|
|
|
15193
15706
|
...process.env,
|
|
15194
15707
|
[WORKER_ENV_FLAG2]: "1",
|
|
15195
15708
|
[TOOL_ENV_FLAG2]: tool,
|
|
15196
|
-
[
|
|
15709
|
+
[FLOW_ID_ENV2]: flowId
|
|
15197
15710
|
}
|
|
15198
15711
|
}
|
|
15199
15712
|
);
|
|
@@ -15215,7 +15728,7 @@ async function runGitTraces() {
|
|
|
15215
15728
|
}
|
|
15216
15729
|
}
|
|
15217
15730
|
async function runGitTracesWorker() {
|
|
15218
|
-
setLogPrefix(`[${process.env[
|
|
15731
|
+
setLogPrefix(`[${process.env[FLOW_ID_ENV2] ?? newFlowId2()}]`);
|
|
15219
15732
|
const tool = process.env[TOOL_ENV_FLAG2] ?? parseToolArg2(process.argv.slice(2)) ?? null;
|
|
15220
15733
|
appendLog(
|
|
15221
15734
|
"info",
|
|
@@ -15297,29 +15810,29 @@ ${stack}` : ""}`
|
|
|
15297
15810
|
}
|
|
15298
15811
|
|
|
15299
15812
|
// src/outputs/zip.ts
|
|
15300
|
-
import
|
|
15301
|
-
import
|
|
15813
|
+
import fs15 from "fs";
|
|
15814
|
+
import path20 from "path";
|
|
15302
15815
|
import archiver2 from "archiver";
|
|
15303
15816
|
|
|
15304
15817
|
// src/outputs/downloads.ts
|
|
15305
15818
|
import { execSync as execSync2 } from "child_process";
|
|
15306
|
-
import
|
|
15307
|
-
import
|
|
15308
|
-
import
|
|
15819
|
+
import fs14 from "fs";
|
|
15820
|
+
import os9 from "os";
|
|
15821
|
+
import path19 from "path";
|
|
15309
15822
|
function getDownloadsFolder() {
|
|
15310
|
-
const home =
|
|
15823
|
+
const home = os9.homedir();
|
|
15311
15824
|
if (process.platform === "linux") {
|
|
15312
15825
|
try {
|
|
15313
15826
|
const xdgDir = execSync2("xdg-user-dir DOWNLOAD", {
|
|
15314
15827
|
encoding: "utf-8",
|
|
15315
15828
|
timeout: 3e3
|
|
15316
15829
|
}).trim();
|
|
15317
|
-
if (xdgDir &&
|
|
15830
|
+
if (xdgDir && fs14.existsSync(xdgDir)) return xdgDir;
|
|
15318
15831
|
} catch {
|
|
15319
15832
|
}
|
|
15320
15833
|
}
|
|
15321
|
-
const downloads =
|
|
15322
|
-
if (
|
|
15834
|
+
const downloads = path19.join(home, "Downloads");
|
|
15835
|
+
if (fs14.existsSync(downloads)) return downloads;
|
|
15323
15836
|
return home;
|
|
15324
15837
|
}
|
|
15325
15838
|
|
|
@@ -15328,11 +15841,11 @@ function sanitizeFilename(name) {
|
|
|
15328
15841
|
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
15329
15842
|
}
|
|
15330
15843
|
function getUniqueFilename(dir, base, ext) {
|
|
15331
|
-
let candidate =
|
|
15332
|
-
if (!
|
|
15844
|
+
let candidate = path20.join(dir, `${base}${ext}`);
|
|
15845
|
+
if (!fs15.existsSync(candidate)) return candidate;
|
|
15333
15846
|
let i = 1;
|
|
15334
|
-
while (
|
|
15335
|
-
candidate =
|
|
15847
|
+
while (fs15.existsSync(candidate)) {
|
|
15848
|
+
candidate = path20.join(dir, `${base}-${i}${ext}`);
|
|
15336
15849
|
i++;
|
|
15337
15850
|
}
|
|
15338
15851
|
return candidate;
|
|
@@ -15342,13 +15855,13 @@ var ZipOutput = class {
|
|
|
15342
15855
|
label = "Save as .zip to Downloads";
|
|
15343
15856
|
async emit(group, options) {
|
|
15344
15857
|
const downloadsDir = getDownloadsFolder();
|
|
15345
|
-
const repoName = sanitizeFilename(
|
|
15858
|
+
const repoName = sanitizeFilename(path20.basename(group.repoPath));
|
|
15346
15859
|
const timeRange = options.timeRange;
|
|
15347
15860
|
const rangePart = timeRange?.label ?? "all";
|
|
15348
15861
|
const epochSeconds = Math.floor(Date.now() / 1e3);
|
|
15349
15862
|
const baseName = `logs-${repoName}-${rangePart}-${epochSeconds}`;
|
|
15350
15863
|
const outputPath = getUniqueFilename(downloadsDir, baseName, ".zip");
|
|
15351
|
-
const output =
|
|
15864
|
+
const output = fs15.createWriteStream(outputPath);
|
|
15352
15865
|
const archive = archiver2("zip", { zlib: { level: 6 } });
|
|
15353
15866
|
const done = new Promise((resolve, reject) => {
|
|
15354
15867
|
output.on("close", resolve);
|
|
@@ -15542,15 +16055,15 @@ async function confirmExport(group, output) {
|
|
|
15542
16055
|
}
|
|
15543
16056
|
|
|
15544
16057
|
// src/sources/claude.ts
|
|
15545
|
-
import
|
|
15546
|
-
import
|
|
15547
|
-
import
|
|
16058
|
+
import fs16 from "fs";
|
|
16059
|
+
import os10 from "os";
|
|
16060
|
+
import path21 from "path";
|
|
15548
16061
|
import readline from "readline";
|
|
15549
16062
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
|
|
15550
16063
|
async function resolveRepoPath(projectDir) {
|
|
15551
|
-
const indexPath =
|
|
16064
|
+
const indexPath = path21.join(projectDir, "sessions-index.json");
|
|
15552
16065
|
try {
|
|
15553
|
-
const raw = await
|
|
16066
|
+
const raw = await fs16.promises.readFile(indexPath, "utf-8");
|
|
15554
16067
|
const data = JSON.parse(raw);
|
|
15555
16068
|
if (data.originalPath && typeof data.originalPath === "string") {
|
|
15556
16069
|
return data.originalPath;
|
|
@@ -15558,12 +16071,12 @@ async function resolveRepoPath(projectDir) {
|
|
|
15558
16071
|
} catch {
|
|
15559
16072
|
}
|
|
15560
16073
|
const cwdCounts = /* @__PURE__ */ new Map();
|
|
15561
|
-
const entries = await
|
|
16074
|
+
const entries = await fs16.promises.readdir(projectDir, {
|
|
15562
16075
|
withFileTypes: true
|
|
15563
16076
|
});
|
|
15564
16077
|
for (const entry of entries) {
|
|
15565
16078
|
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
15566
|
-
const cwd = await extractCwdFromJsonl(
|
|
16079
|
+
const cwd = await extractCwdFromJsonl(path21.join(projectDir, entry.name));
|
|
15567
16080
|
if (cwd) {
|
|
15568
16081
|
cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
|
|
15569
16082
|
}
|
|
@@ -15582,7 +16095,7 @@ async function resolveRepoPath(projectDir) {
|
|
|
15582
16095
|
return null;
|
|
15583
16096
|
}
|
|
15584
16097
|
async function extractCwdFromJsonl(filePath) {
|
|
15585
|
-
const stream =
|
|
16098
|
+
const stream = fs16.createReadStream(filePath, { encoding: "utf-8" });
|
|
15586
16099
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15587
16100
|
try {
|
|
15588
16101
|
for await (const line of rl) {
|
|
@@ -15604,12 +16117,12 @@ async function extractCwdFromJsonl(filePath) {
|
|
|
15604
16117
|
async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
|
|
15605
16118
|
let entries;
|
|
15606
16119
|
try {
|
|
15607
|
-
entries = await
|
|
16120
|
+
entries = await fs16.promises.readdir(dir, { withFileTypes: true });
|
|
15608
16121
|
} catch {
|
|
15609
16122
|
return;
|
|
15610
16123
|
}
|
|
15611
16124
|
for (const entry of entries) {
|
|
15612
|
-
const fullPath =
|
|
16125
|
+
const fullPath = path21.join(dir, entry.name);
|
|
15613
16126
|
if (entry.isDirectory()) {
|
|
15614
16127
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
15615
16128
|
await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
|
|
@@ -15631,19 +16144,19 @@ function fallbackDecode(encodedName) {
|
|
|
15631
16144
|
var ClaudeSource = class {
|
|
15632
16145
|
name = "claude";
|
|
15633
16146
|
async scan() {
|
|
15634
|
-
const baseDir =
|
|
16147
|
+
const baseDir = path21.join(os10.homedir(), ".claude", "projects");
|
|
15635
16148
|
try {
|
|
15636
|
-
await
|
|
16149
|
+
await fs16.promises.access(baseDir);
|
|
15637
16150
|
} catch {
|
|
15638
16151
|
return [];
|
|
15639
16152
|
}
|
|
15640
|
-
const projectDirs = await
|
|
16153
|
+
const projectDirs = await fs16.promises.readdir(baseDir, {
|
|
15641
16154
|
withFileTypes: true
|
|
15642
16155
|
});
|
|
15643
16156
|
const dirEntries = projectDirs.filter((d) => d.isDirectory());
|
|
15644
16157
|
const resultArrays = await Promise.all(
|
|
15645
16158
|
dirEntries.map(async (dir) => {
|
|
15646
|
-
const projectPath =
|
|
16159
|
+
const projectPath = path21.join(baseDir, dir.name);
|
|
15647
16160
|
const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
|
|
15648
16161
|
const files = [];
|
|
15649
16162
|
await collectFiles(
|
|
@@ -15661,12 +16174,12 @@ var ClaudeSource = class {
|
|
|
15661
16174
|
};
|
|
15662
16175
|
|
|
15663
16176
|
// src/sources/codex.ts
|
|
15664
|
-
import
|
|
15665
|
-
import
|
|
15666
|
-
import
|
|
16177
|
+
import fs17 from "fs";
|
|
16178
|
+
import os11 from "os";
|
|
16179
|
+
import path22 from "path";
|
|
15667
16180
|
import readline2 from "readline";
|
|
15668
16181
|
async function parseSessionMeta(filePath) {
|
|
15669
|
-
const stream =
|
|
16182
|
+
const stream = fs17.createReadStream(filePath, { encoding: "utf-8" });
|
|
15670
16183
|
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15671
16184
|
try {
|
|
15672
16185
|
for await (const line of rl) {
|
|
@@ -15691,12 +16204,12 @@ async function findJsonlFiles(dir) {
|
|
|
15691
16204
|
async function walk(d) {
|
|
15692
16205
|
let entries;
|
|
15693
16206
|
try {
|
|
15694
|
-
entries = await
|
|
16207
|
+
entries = await fs17.promises.readdir(d, { withFileTypes: true });
|
|
15695
16208
|
} catch {
|
|
15696
16209
|
return;
|
|
15697
16210
|
}
|
|
15698
16211
|
for (const entry of entries) {
|
|
15699
|
-
const full =
|
|
16212
|
+
const full = path22.join(d, entry.name);
|
|
15700
16213
|
if (entry.isDirectory()) {
|
|
15701
16214
|
await walk(full);
|
|
15702
16215
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -15710,11 +16223,11 @@ async function findJsonlFiles(dir) {
|
|
|
15710
16223
|
async function loadHistory(historyPath) {
|
|
15711
16224
|
const map = /* @__PURE__ */ new Map();
|
|
15712
16225
|
try {
|
|
15713
|
-
await
|
|
16226
|
+
await fs17.promises.access(historyPath);
|
|
15714
16227
|
} catch {
|
|
15715
16228
|
return map;
|
|
15716
16229
|
}
|
|
15717
|
-
const stream =
|
|
16230
|
+
const stream = fs17.createReadStream(historyPath, { encoding: "utf-8" });
|
|
15718
16231
|
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15719
16232
|
try {
|
|
15720
16233
|
for await (const line of rl) {
|
|
@@ -15741,14 +16254,14 @@ async function loadHistory(historyPath) {
|
|
|
15741
16254
|
var CodexSource = class {
|
|
15742
16255
|
name = "codex";
|
|
15743
16256
|
async scan() {
|
|
15744
|
-
const codexDir =
|
|
15745
|
-
const sessionsDir =
|
|
16257
|
+
const codexDir = path22.join(os11.homedir(), ".codex");
|
|
16258
|
+
const sessionsDir = path22.join(codexDir, "sessions");
|
|
15746
16259
|
try {
|
|
15747
|
-
await
|
|
16260
|
+
await fs17.promises.access(sessionsDir);
|
|
15748
16261
|
} catch {
|
|
15749
16262
|
return [];
|
|
15750
16263
|
}
|
|
15751
|
-
const historyPath =
|
|
16264
|
+
const historyPath = path22.join(codexDir, "history.jsonl");
|
|
15752
16265
|
const [jsonlFiles, historyMap] = await Promise.all([
|
|
15753
16266
|
findJsonlFiles(sessionsDir),
|
|
15754
16267
|
loadHistory(historyPath)
|
|
@@ -15771,8 +16284,8 @@ var CodexSource = class {
|
|
|
15771
16284
|
});
|
|
15772
16285
|
const historyLines = historyMap.get(meta.sessionId);
|
|
15773
16286
|
if (historyLines) {
|
|
15774
|
-
const sessionDir =
|
|
15775
|
-
const historyAbsPath =
|
|
16287
|
+
const sessionDir = path22.relative(sessionsDir, path22.dirname(filePath));
|
|
16288
|
+
const historyAbsPath = path22.join(
|
|
15776
16289
|
sessionsDir,
|
|
15777
16290
|
sessionDir,
|
|
15778
16291
|
`history-${meta.sessionId}.jsonl`
|
|
@@ -15792,18 +16305,18 @@ var CodexSource = class {
|
|
|
15792
16305
|
};
|
|
15793
16306
|
|
|
15794
16307
|
// src/sources/copilotChat.ts
|
|
15795
|
-
import
|
|
15796
|
-
import
|
|
15797
|
-
import
|
|
16308
|
+
import fs18 from "fs";
|
|
16309
|
+
import os12 from "os";
|
|
16310
|
+
import path23 from "path";
|
|
15798
16311
|
import { fileURLToPath } from "url";
|
|
15799
16312
|
function vsCodeUserDirs() {
|
|
15800
|
-
const home =
|
|
16313
|
+
const home = os12.homedir();
|
|
15801
16314
|
const dirs = [
|
|
15802
|
-
|
|
15803
|
-
|
|
16315
|
+
path23.join(home, "Library", "Application Support", "Code", "User"),
|
|
16316
|
+
path23.join(home, ".config", "Code", "User")
|
|
15804
16317
|
];
|
|
15805
16318
|
if (process.env.APPDATA) {
|
|
15806
|
-
dirs.push(
|
|
16319
|
+
dirs.push(path23.join(process.env.APPDATA, "Code", "User"));
|
|
15807
16320
|
}
|
|
15808
16321
|
return dirs;
|
|
15809
16322
|
}
|
|
@@ -15818,7 +16331,7 @@ function uriToFsPath(uri) {
|
|
|
15818
16331
|
async function readWorkspaceFolder(workspaceJsonPath) {
|
|
15819
16332
|
let raw;
|
|
15820
16333
|
try {
|
|
15821
|
-
raw = await
|
|
16334
|
+
raw = await fs18.promises.readFile(workspaceJsonPath, "utf-8");
|
|
15822
16335
|
} catch {
|
|
15823
16336
|
return null;
|
|
15824
16337
|
}
|
|
@@ -15840,10 +16353,10 @@ var CopilotChatSource = class {
|
|
|
15840
16353
|
async scan() {
|
|
15841
16354
|
const results = [];
|
|
15842
16355
|
for (const userDir of vsCodeUserDirs()) {
|
|
15843
|
-
const workspaceStorage =
|
|
16356
|
+
const workspaceStorage = path23.join(userDir, "workspaceStorage");
|
|
15844
16357
|
let hashDirs;
|
|
15845
16358
|
try {
|
|
15846
|
-
hashDirs = await
|
|
16359
|
+
hashDirs = await fs18.promises.readdir(workspaceStorage, {
|
|
15847
16360
|
withFileTypes: true
|
|
15848
16361
|
});
|
|
15849
16362
|
} catch {
|
|
@@ -15851,22 +16364,22 @@ var CopilotChatSource = class {
|
|
|
15851
16364
|
}
|
|
15852
16365
|
for (const hash of hashDirs) {
|
|
15853
16366
|
if (!hash.isDirectory()) continue;
|
|
15854
|
-
const wsRoot =
|
|
15855
|
-
const transcriptsDir =
|
|
16367
|
+
const wsRoot = path23.join(workspaceStorage, hash.name);
|
|
16368
|
+
const transcriptsDir = path23.join(
|
|
15856
16369
|
wsRoot,
|
|
15857
16370
|
"GitHub.copilot-chat",
|
|
15858
16371
|
"transcripts"
|
|
15859
16372
|
);
|
|
15860
16373
|
let transcriptEntries;
|
|
15861
16374
|
try {
|
|
15862
|
-
transcriptEntries = await
|
|
16375
|
+
transcriptEntries = await fs18.promises.readdir(transcriptsDir, {
|
|
15863
16376
|
withFileTypes: true
|
|
15864
16377
|
});
|
|
15865
16378
|
} catch {
|
|
15866
16379
|
continue;
|
|
15867
16380
|
}
|
|
15868
16381
|
const repoPath = await readWorkspaceFolder(
|
|
15869
|
-
|
|
16382
|
+
path23.join(wsRoot, "workspace.json")
|
|
15870
16383
|
);
|
|
15871
16384
|
if (!repoPath) continue;
|
|
15872
16385
|
for (const entry of transcriptEntries) {
|
|
@@ -15874,7 +16387,7 @@ var CopilotChatSource = class {
|
|
|
15874
16387
|
const sessionId = entry.name.slice(0, -".jsonl".length);
|
|
15875
16388
|
results.push({
|
|
15876
16389
|
sourceName: this.name,
|
|
15877
|
-
absolutePath:
|
|
16390
|
+
absolutePath: path23.join(transcriptsDir, entry.name),
|
|
15878
16391
|
repoPath,
|
|
15879
16392
|
metadata: { sessionId }
|
|
15880
16393
|
});
|
|
@@ -15914,7 +16427,7 @@ function reportRedactionStats(noun, stats) {
|
|
|
15914
16427
|
async function filterByTimeRange(group, range) {
|
|
15915
16428
|
const results = await Promise.all(
|
|
15916
16429
|
group.files.map(
|
|
15917
|
-
(f) =>
|
|
16430
|
+
(f) => fs19.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
|
|
15918
16431
|
)
|
|
15919
16432
|
);
|
|
15920
16433
|
const filtered = [];
|
|
@@ -15941,10 +16454,10 @@ async function runInteractive() {
|
|
|
15941
16454
|
s.start(`Scanning ${source.name} logs...`);
|
|
15942
16455
|
const allFiles = await source.scan();
|
|
15943
16456
|
const allGroups = await mergeByRepo(allFiles);
|
|
15944
|
-
const repoRoot =
|
|
16457
|
+
const repoRoot = path24.resolve(repo.root);
|
|
15945
16458
|
const matching = allGroups.filter((g) => {
|
|
15946
|
-
const resolved =
|
|
15947
|
-
return resolved === repoRoot || resolved.startsWith(repoRoot +
|
|
16459
|
+
const resolved = path24.resolve(g.repoPath);
|
|
16460
|
+
return resolved === repoRoot || resolved.startsWith(repoRoot + path24.sep);
|
|
15948
16461
|
});
|
|
15949
16462
|
if (matching.length === 0) {
|
|
15950
16463
|
s.stop(`No ${source.name} logs found for ${repo.name}.`);
|
|
@@ -15975,7 +16488,7 @@ async function runInteractive() {
|
|
|
15975
16488
|
}
|
|
15976
16489
|
}
|
|
15977
16490
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
15978
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
16491
|
+
const envFilePaths = envFileNames.map((n) => path24.join(repoRoot, n));
|
|
15979
16492
|
const additionalFiles = await promptSecretFiles(envFileNames);
|
|
15980
16493
|
const secretResult = await collectSecrets(
|
|
15981
16494
|
repoRoot,
|