agent-trajectories 0.5.2 → 0.5.3

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/index.js CHANGED
@@ -49,18 +49,20 @@ var TrajectoryStatusSchema = z.enum([
49
49
  "completed",
50
50
  "abandoned"
51
51
  ]);
52
- var TrajectoryEventTypeSchema = z.enum([
53
- "prompt",
54
- "thinking",
55
- "tool_call",
56
- "tool_result",
57
- "message_sent",
58
- "message_received",
59
- "decision",
60
- "finding",
61
- "reflection",
62
- "note",
63
- "error"
52
+ var TrajectoryEventTypeSchema = z.union([
53
+ z.literal("prompt"),
54
+ z.literal("thinking"),
55
+ z.literal("tool_call"),
56
+ z.literal("tool_result"),
57
+ z.literal("message_sent"),
58
+ z.literal("message_received"),
59
+ z.literal("decision"),
60
+ z.literal("finding"),
61
+ z.literal("reflection"),
62
+ z.literal("note"),
63
+ z.literal("error"),
64
+ z.string()
65
+ // Allow event types emitted by other tools (e.g. agent-relay's completion-evidence / completion-marker). Downstream code filters to known types.
64
66
  ]);
65
67
  var EventSignificanceSchema = z.enum([
66
68
  "low",
@@ -162,6 +164,7 @@ var TrajectorySchema = z.object({
162
164
  commits: z.array(z.string()),
163
165
  filesChanged: z.array(z.string()),
164
166
  projectId: z.string(),
167
+ workflowId: z.string().optional(),
165
168
  tags: z.array(z.string()),
166
169
  _trace: TrajectoryTraceRefSchema.optional()
167
170
  });
@@ -880,9 +883,1132 @@ function registerAbandonCommand(program2) {
880
883
  }
881
884
 
882
885
  // src/cli/commands/compact.ts
883
- import { execSync } from "child_process";
884
- import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
886
+ import { execFileSync } from "child_process";
887
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
888
+ import { dirname, join as join4 } from "path";
889
+
890
+ // src/compact/config.ts
891
+ import { existsSync as existsSync2, readFileSync } from "fs";
885
892
  import { join as join2 } from "path";
893
+ var DEFAULT_CONFIG = {
894
+ provider: "auto",
895
+ model: void 0,
896
+ maxInputTokens: 3e4,
897
+ maxOutputTokens: 4e3,
898
+ temperature: 0.3
899
+ };
900
+ function getCompactionConfig() {
901
+ const fileConfig = loadFileConfig();
902
+ return {
903
+ provider: readStringEnv("TRAJECTORIES_LLM_PROVIDER") ?? readString(fileConfig.provider) ?? DEFAULT_CONFIG.provider,
904
+ model: readStringEnv("TRAJECTORIES_LLM_MODEL") ?? readString(fileConfig.model) ?? DEFAULT_CONFIG.model,
905
+ maxInputTokens: readNumberEnv("TRAJECTORIES_LLM_MAX_INPUT_TOKENS") ?? readNumber(fileConfig.maxInputTokens) ?? DEFAULT_CONFIG.maxInputTokens,
906
+ maxOutputTokens: readNumberEnv("TRAJECTORIES_LLM_MAX_OUTPUT_TOKENS") ?? readNumber(fileConfig.maxOutputTokens) ?? DEFAULT_CONFIG.maxOutputTokens,
907
+ temperature: readNumberEnv("TRAJECTORIES_LLM_TEMPERATURE") ?? readNumber(fileConfig.temperature) ?? DEFAULT_CONFIG.temperature
908
+ };
909
+ }
910
+ function loadFileConfig() {
911
+ const configPath = join2(getPrimaryConfigDir(), "config.json");
912
+ if (!existsSync2(configPath)) {
913
+ return {};
914
+ }
915
+ try {
916
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
917
+ if (!isRecord(raw)) {
918
+ return {};
919
+ }
920
+ const merged = {};
921
+ for (const section of [raw, raw.compaction, raw.llm]) {
922
+ if (!isRecord(section)) {
923
+ continue;
924
+ }
925
+ for (const [key, value] of Object.entries(section)) {
926
+ if (key === "compaction" || key === "llm") {
927
+ continue;
928
+ }
929
+ merged[key] = value;
930
+ }
931
+ }
932
+ return {
933
+ provider: readString(merged.provider),
934
+ model: readString(merged.model),
935
+ maxInputTokens: readNumber(merged.maxInputTokens),
936
+ maxOutputTokens: readNumber(merged.maxOutputTokens),
937
+ temperature: readNumber(merged.temperature)
938
+ };
939
+ } catch {
940
+ return {};
941
+ }
942
+ }
943
+ function getPrimaryConfigDir() {
944
+ const searchPaths = getSearchPaths();
945
+ return searchPaths[0] ?? join2(process.cwd(), ".trajectories");
946
+ }
947
+ function readStringEnv(name) {
948
+ return readString(process.env[name]);
949
+ }
950
+ function readNumberEnv(name) {
951
+ return readNumber(process.env[name]);
952
+ }
953
+ function readString(value) {
954
+ if (typeof value !== "string") {
955
+ return void 0;
956
+ }
957
+ const trimmed = value.trim();
958
+ return trimmed.length > 0 ? trimmed : void 0;
959
+ }
960
+ function readNumber(value) {
961
+ if (typeof value === "number" && Number.isFinite(value)) {
962
+ return value;
963
+ }
964
+ if (typeof value !== "string") {
965
+ return void 0;
966
+ }
967
+ const trimmed = value.trim();
968
+ if (trimmed.length === 0) {
969
+ return void 0;
970
+ }
971
+ const parsed = Number(trimmed);
972
+ return Number.isFinite(parsed) ? parsed : void 0;
973
+ }
974
+ function isRecord(value) {
975
+ return typeof value === "object" && value !== null;
976
+ }
977
+
978
+ // src/compact/markdown.ts
979
+ function generateCompactionMarkdown(compacted) {
980
+ const dateRange = `${formatDate2(compacted.dateRange.start)} - ${formatDate2(compacted.dateRange.end)}`;
981
+ const agents = compacted.summary.uniqueAgents.length > 0 ? compacted.summary.uniqueAgents.join(", ") : "None";
982
+ const decisionRows = compacted.decisions.length > 0 ? compacted.decisions.map(
983
+ (decision) => `| ${escapeTableCell(decision.question)} | ${escapeTableCell(decision.chosen)} | ${escapeTableCell(decision.impact)} |`
984
+ ).join("\n") : "| None identified | | |";
985
+ const conventions = compacted.conventions.length > 0 ? compacted.conventions.map(
986
+ (convention) => `- **${convention.pattern || "Unnamed pattern"}**: ${convention.rationale || "No rationale captured."} (scope: ${convention.scope || "unspecified"})`
987
+ ).join("\n") : "- None established.";
988
+ const lessons = compacted.lessons.length > 0 ? compacted.lessons.map((lesson) => {
989
+ const context = lesson.context ? ` (${lesson.context})` : "";
990
+ const recommendation = lesson.recommendation ? ` - ${lesson.recommendation}` : "";
991
+ return `- ${lesson.lesson}${context}${recommendation}`;
992
+ }).join("\n") : "- None captured.";
993
+ const openQuestions = compacted.openQuestions.length > 0 ? compacted.openQuestions.map((question) => `- ${question}`).join("\n") : "- None.";
994
+ return [
995
+ `# Trajectory Compaction: ${dateRange}`,
996
+ "",
997
+ "## Summary",
998
+ compacted.narrative || "No narrative available.",
999
+ "",
1000
+ `## Key Decisions (${compacted.decisions.length})`,
1001
+ "| Question | Decision | Impact |",
1002
+ "|----------|----------|--------|",
1003
+ decisionRows,
1004
+ "",
1005
+ "## Conventions Established",
1006
+ conventions,
1007
+ "",
1008
+ "## Lessons Learned",
1009
+ lessons,
1010
+ "",
1011
+ "## Open Questions",
1012
+ openQuestions,
1013
+ "",
1014
+ "## Stats",
1015
+ `- Sessions: ${compacted.sourceTrajectories.length}, Agents: ${agents}, Files: ${compacted.filesAffected.length}, Commits: ${compacted.commits.length}`,
1016
+ `- Date range: ${compacted.dateRange.start} - ${compacted.dateRange.end}`
1017
+ ].join("\n");
1018
+ }
1019
+ function formatDate2(value) {
1020
+ const date = new Date(value);
1021
+ if (Number.isNaN(date.getTime())) {
1022
+ return value;
1023
+ }
1024
+ return date.toISOString().slice(0, 10);
1025
+ }
1026
+ function escapeTableCell(value) {
1027
+ return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
1028
+ }
1029
+
1030
+ // src/compact/parser.ts
1031
+ function parseCompactionResponse(llmOutput) {
1032
+ const trimmed = llmOutput.trim();
1033
+ const parsedJson = parseJsonCandidate(trimmed) ?? parseJsonCandidate(extractFirstMarkdownJsonBlock(trimmed)) ?? parseJsonCandidate(extractBalancedJsonObject(trimmed));
1034
+ if (parsedJson) {
1035
+ return normalizeCompactionOutput(parsedJson, trimmed);
1036
+ }
1037
+ return normalizeCompactionOutput(extractFromProse(trimmed), trimmed);
1038
+ }
1039
+ function mergeCompactionWithMetadata(metadata, llmOutput) {
1040
+ return {
1041
+ ...metadata,
1042
+ ...llmOutput
1043
+ };
1044
+ }
1045
+ function parseJsonCandidate(candidate) {
1046
+ if (!candidate) {
1047
+ return null;
1048
+ }
1049
+ try {
1050
+ return JSON.parse(candidate);
1051
+ } catch {
1052
+ return null;
1053
+ }
1054
+ }
1055
+ function extractFirstMarkdownJsonBlock(text) {
1056
+ const match = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
1057
+ return match ? match[1].trim() : null;
1058
+ }
1059
+ function extractBalancedJsonObject(text) {
1060
+ const start = text.indexOf("{");
1061
+ if (start === -1) {
1062
+ return null;
1063
+ }
1064
+ let depth = 0;
1065
+ let inString = false;
1066
+ let escaped = false;
1067
+ for (let index = start; index < text.length; index += 1) {
1068
+ const char = text[index];
1069
+ if (escaped) {
1070
+ escaped = false;
1071
+ continue;
1072
+ }
1073
+ if (char === "\\") {
1074
+ escaped = true;
1075
+ continue;
1076
+ }
1077
+ if (char === '"') {
1078
+ inString = !inString;
1079
+ continue;
1080
+ }
1081
+ if (inString) {
1082
+ continue;
1083
+ }
1084
+ if (char === "{") {
1085
+ depth += 1;
1086
+ } else if (char === "}") {
1087
+ depth -= 1;
1088
+ if (depth === 0) {
1089
+ return text.slice(start, index + 1);
1090
+ }
1091
+ }
1092
+ }
1093
+ return null;
1094
+ }
1095
+ function extractFromProse(text) {
1096
+ const sections = splitSections(text);
1097
+ const narrativeSection = sections.narrative ?? sections.summary ?? leadingNarrative(text);
1098
+ return {
1099
+ narrative: normalizeText(narrativeSection),
1100
+ decisions: parseDecisionSection(
1101
+ sections["key decisions"] ?? sections.decisions ?? ""
1102
+ ),
1103
+ conventions: parseConventionSection(
1104
+ sections["conventions established"] ?? sections.conventions ?? ""
1105
+ ),
1106
+ lessons: parseLessonSection(
1107
+ sections["lessons learned"] ?? sections.lessons ?? ""
1108
+ ),
1109
+ openQuestions: parseStringList(
1110
+ sections["open questions"] ?? sections.questions ?? ""
1111
+ )
1112
+ };
1113
+ }
1114
+ function splitSections(text) {
1115
+ const matches = [...text.matchAll(/^##+\s+(.+?)\s*$/gm)];
1116
+ const sections = {};
1117
+ for (let index = 0; index < matches.length; index += 1) {
1118
+ const current = matches[index];
1119
+ const next = matches[index + 1];
1120
+ const title = normalizeHeading(current[1]);
1121
+ const start = current.index === void 0 ? 0 : current.index + current[0].length;
1122
+ const end = next?.index ?? text.length;
1123
+ sections[title] = text.slice(start, end).trim();
1124
+ }
1125
+ return sections;
1126
+ }
1127
+ function normalizeHeading(value) {
1128
+ return value.toLowerCase().replace(/\(\d+\)/g, "").replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
1129
+ }
1130
+ function leadingNarrative(text) {
1131
+ const beforeHeading = text.split(/^##+\s+/m, 1)[0] ?? "";
1132
+ const withoutCode = beforeHeading.replace(/```[\s\S]*?```/g, "").trim();
1133
+ return withoutCode;
1134
+ }
1135
+ function normalizeCompactionOutput(raw, fallbackNarrativeSource) {
1136
+ const candidate = isRecord2(raw) ? raw : {};
1137
+ const narrative = normalizeText(
1138
+ typeof candidate.narrative === "string" ? candidate.narrative : typeof candidate.summary === "string" ? candidate.summary : typeof candidate.overview === "string" ? candidate.overview : leadingNarrative(fallbackNarrativeSource)
1139
+ );
1140
+ return {
1141
+ narrative: narrative || normalizeText(fallbackNarrativeSource) || "No narrative provided.",
1142
+ decisions: normalizeDecisionArray(candidate.decisions),
1143
+ conventions: normalizeConventionArray(candidate.conventions),
1144
+ lessons: normalizeLessonArray(candidate.lessons),
1145
+ openQuestions: normalizeStringArray(
1146
+ candidate.openQuestions ?? candidate.open_questions ?? candidate.questions
1147
+ )
1148
+ };
1149
+ }
1150
+ function normalizeDecisionArray(value) {
1151
+ if (!Array.isArray(value)) {
1152
+ return [];
1153
+ }
1154
+ return value.map((entry) => {
1155
+ if (typeof entry === "string") {
1156
+ return {
1157
+ question: normalizeText(entry),
1158
+ chosen: "",
1159
+ reasoning: "",
1160
+ impact: ""
1161
+ };
1162
+ }
1163
+ if (!isRecord2(entry)) {
1164
+ return null;
1165
+ }
1166
+ return {
1167
+ question: readString2(entry, ["question", "prompt", "topic"]),
1168
+ chosen: readString2(entry, ["chosen", "decision", "answer"]),
1169
+ reasoning: readString2(entry, ["reasoning", "why", "rationale"]),
1170
+ impact: readString2(entry, ["impact", "result", "outcome"])
1171
+ };
1172
+ }).filter((entry) => {
1173
+ return entry !== null && hasContent(Object.values(entry));
1174
+ });
1175
+ }
1176
+ function normalizeConventionArray(value) {
1177
+ if (!Array.isArray(value)) {
1178
+ return [];
1179
+ }
1180
+ return value.map((entry) => {
1181
+ if (typeof entry === "string") {
1182
+ return {
1183
+ pattern: normalizeText(entry),
1184
+ rationale: "",
1185
+ scope: ""
1186
+ };
1187
+ }
1188
+ if (!isRecord2(entry)) {
1189
+ return null;
1190
+ }
1191
+ return {
1192
+ pattern: readString2(entry, ["pattern", "rule", "convention"]),
1193
+ rationale: readString2(entry, ["rationale", "reasoning", "why"]),
1194
+ scope: readString2(entry, ["scope", "appliesTo", "applies_to"])
1195
+ };
1196
+ }).filter((entry) => {
1197
+ return entry !== null && hasContent(Object.values(entry));
1198
+ });
1199
+ }
1200
+ function normalizeLessonArray(value) {
1201
+ if (!Array.isArray(value)) {
1202
+ return [];
1203
+ }
1204
+ return value.map((entry) => {
1205
+ if (typeof entry === "string") {
1206
+ return {
1207
+ lesson: normalizeText(entry),
1208
+ context: "",
1209
+ recommendation: ""
1210
+ };
1211
+ }
1212
+ if (!isRecord2(entry)) {
1213
+ return null;
1214
+ }
1215
+ return {
1216
+ lesson: readString2(entry, ["lesson", "learning", "takeaway"]),
1217
+ context: readString2(entry, ["context", "situation", "when"]),
1218
+ recommendation: readString2(entry, [
1219
+ "recommendation",
1220
+ "suggestion",
1221
+ "nextStep",
1222
+ "next_step"
1223
+ ])
1224
+ };
1225
+ }).filter((entry) => {
1226
+ return entry !== null && hasContent(Object.values(entry));
1227
+ });
1228
+ }
1229
+ function normalizeStringArray(value) {
1230
+ if (!Array.isArray(value)) {
1231
+ return [];
1232
+ }
1233
+ return value.map((entry) => typeof entry === "string" ? normalizeText(entry) : "").filter(Boolean);
1234
+ }
1235
+ function parseDecisionSection(section) {
1236
+ const tableDecisions = parseMarkdownTable(section).map((row) => ({
1237
+ question: row[0] ?? "",
1238
+ chosen: row[1] ?? "",
1239
+ reasoning: row[2] ?? "",
1240
+ impact: row[3] ?? row[2] ?? ""
1241
+ }));
1242
+ if (tableDecisions.length > 0) {
1243
+ return tableDecisions.filter((entry) => hasContent(Object.values(entry)));
1244
+ }
1245
+ return parseListItems(section).map((item) => {
1246
+ const fields = parseFieldMap(item);
1247
+ return {
1248
+ question: fields.question ?? fields.prompt ?? fields.topic ?? fields.title ?? item,
1249
+ chosen: fields.chosen ?? fields.decision ?? fields.answer ?? "",
1250
+ reasoning: fields.reasoning ?? fields.rationale ?? fields.why ?? "",
1251
+ impact: fields.impact ?? fields.outcome ?? fields.result ?? ""
1252
+ };
1253
+ }).filter((entry) => hasContent(Object.values(entry)));
1254
+ }
1255
+ function parseConventionSection(section) {
1256
+ return parseListItems(section).map((item) => {
1257
+ const emphasized = item.match(/^\*\*(.+?)\*\*:\s*(.+)$/);
1258
+ const scopeMatch = item.match(/\((?:scope|applies to):\s*([^)]+)\)\s*$/i);
1259
+ const withoutScope = scopeMatch ? item.slice(0, scopeMatch.index).trim() : item;
1260
+ if (emphasized) {
1261
+ return {
1262
+ pattern: normalizeText(emphasized[1]),
1263
+ rationale: normalizeText(
1264
+ withoutScope.replace(/^\*\*(.+?)\*\*:\s*/, "")
1265
+ ),
1266
+ scope: normalizeText(scopeMatch?.[1] ?? "")
1267
+ };
1268
+ }
1269
+ const fields = parseFieldMap(item);
1270
+ return {
1271
+ pattern: fields.pattern ?? fields.convention ?? fields.rule ?? item,
1272
+ rationale: fields.rationale ?? fields.reasoning ?? fields.why ?? "",
1273
+ scope: fields.scope ?? fields.applies ?? ""
1274
+ };
1275
+ }).filter((entry) => hasContent(Object.values(entry)));
1276
+ }
1277
+ function parseLessonSection(section) {
1278
+ return parseListItems(section).map((item) => {
1279
+ const fields = parseFieldMap(item);
1280
+ const dashParts = item.split(/\s[—-]\s/, 2);
1281
+ return {
1282
+ lesson: fields.lesson ?? fields.learning ?? fields.takeaway ?? dashParts[0] ?? item,
1283
+ context: fields.context ?? "",
1284
+ recommendation: fields.recommendation ?? fields.suggestion ?? fields.nextstep ?? dashParts[1] ?? ""
1285
+ };
1286
+ }).filter((entry) => hasContent(Object.values(entry)));
1287
+ }
1288
+ function parseStringList(section) {
1289
+ return parseListItems(section).map(normalizeText).filter(Boolean);
1290
+ }
1291
+ function parseMarkdownTable(section) {
1292
+ const lines = section.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("|"));
1293
+ if (lines.length < 2) {
1294
+ return [];
1295
+ }
1296
+ return lines.slice(1).filter((line) => !/^\|?\s*:?-{3,}/.test(line.replace(/\|/g, ""))).map(
1297
+ (line) => line.split("|").slice(1, -1).map((cell) => normalizeText(cell))
1298
+ );
1299
+ }
1300
+ function parseListItems(section) {
1301
+ return section.split("\n").map((line) => line.trim()).filter((line) => /^[-*] |\d+\.\s/.test(line)).map((line) => line.replace(/^[-*]\s+|\d+\.\s+/, "").trim()).filter(Boolean);
1302
+ }
1303
+ function parseFieldMap(item) {
1304
+ const normalized = item.replace(/\s+\|\s+/g, "; ");
1305
+ const segments = normalized.split(/;\s+/);
1306
+ const fields = {};
1307
+ for (const segment of segments) {
1308
+ const match = segment.match(/^([A-Za-z ]+):\s*(.+)$/);
1309
+ if (!match) {
1310
+ continue;
1311
+ }
1312
+ const key = match[1].toLowerCase().replace(/\s+/g, "");
1313
+ fields[key] = normalizeText(match[2]);
1314
+ }
1315
+ return fields;
1316
+ }
1317
+ function readString2(record, keys) {
1318
+ for (const key of keys) {
1319
+ const value = record[key];
1320
+ if (typeof value === "string") {
1321
+ return normalizeText(value);
1322
+ }
1323
+ }
1324
+ return "";
1325
+ }
1326
+ function isRecord2(value) {
1327
+ return typeof value === "object" && value !== null;
1328
+ }
1329
+ function normalizeText(value) {
1330
+ return value.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
1331
+ }
1332
+ function hasContent(values) {
1333
+ return values.some((value) => value.trim().length > 0);
1334
+ }
1335
+
1336
+ // src/compact/prompts.ts
1337
+ var COMPACTION_SYSTEM_PROMPT = `You are a technical analyst reviewing agent work sessions (trajectories).
1338
+ Your job is to produce a concise, insightful summary that captures:
1339
+ - What was accomplished and how
1340
+ - Key decisions and their reasoning
1341
+ - Patterns/conventions established that should be followed in future work
1342
+ - Lessons learned from challenges and failures
1343
+ - Open questions or unresolved issues
1344
+
1345
+ Be specific. Reference actual file paths, function names, and technical details.
1346
+ Don't be generic - this summary replaces the raw data.`;
1347
+ var COMPACTED_OUTPUT_SCHEMA = `{
1348
+ "narrative": "string",
1349
+ "decisions": [
1350
+ {
1351
+ "question": "string",
1352
+ "chosen": "string",
1353
+ "reasoning": "string",
1354
+ "impact": "string"
1355
+ }
1356
+ ],
1357
+ "conventions": [
1358
+ {
1359
+ "pattern": "string",
1360
+ "rationale": "string",
1361
+ "scope": "string"
1362
+ }
1363
+ ],
1364
+ "lessons": [
1365
+ {
1366
+ "lesson": "string",
1367
+ "context": "string",
1368
+ "recommendation": "string"
1369
+ }
1370
+ ],
1371
+ "openQuestions": ["string"]
1372
+ }`;
1373
+ function buildCompactionPrompt(serializedTrajectories, options = {}) {
1374
+ const focusAreas = options.focusAreas && options.focusAreas.length > 0 ? options.focusAreas.map((area) => `- ${area}`).join("\n") : [
1375
+ "- What work was attempted, completed, or abandoned",
1376
+ "- Why specific technical decisions were made",
1377
+ "- Which conventions should carry forward",
1378
+ "- What broke, what worked, and what should change next time"
1379
+ ].join("\n");
1380
+ const maxOutputInstruction = options.maxOutputTokens ? `Keep the full response within approximately ${options.maxOutputTokens} tokens while preserving technical specificity.` : "Keep the response concise, dense with signal, and avoid filler.";
1381
+ const userPrompt = [
1382
+ "Review the following serialized agent trajectories and return a single JSON object.",
1383
+ "The JSON must match this schema exactly:",
1384
+ COMPACTED_OUTPUT_SCHEMA,
1385
+ "",
1386
+ "Requirements:",
1387
+ "- Output raw JSON only. Do not wrap it in markdown fences.",
1388
+ "- `narrative` should be 2-3 tight paragraphs.",
1389
+ "- `decisions`, `conventions`, and `lessons` must always be arrays, even if empty.",
1390
+ "- Prefer concrete file paths, symbols, commands, and implementation details over generic summaries.",
1391
+ maxOutputInstruction,
1392
+ "",
1393
+ "Focus areas:",
1394
+ focusAreas,
1395
+ "",
1396
+ "Serialized trajectories:",
1397
+ serializedTrajectories.trim()
1398
+ ].join("\n");
1399
+ return [
1400
+ {
1401
+ role: "system",
1402
+ content: COMPACTION_SYSTEM_PROMPT
1403
+ },
1404
+ {
1405
+ role: "user",
1406
+ content: userPrompt
1407
+ }
1408
+ ];
1409
+ }
1410
+
1411
+ // src/compact/provider.ts
1412
+ import { execFile, spawn } from "child_process";
1413
+ import { constants, accessSync } from "fs";
1414
+ import { homedir } from "os";
1415
+ import { join as join3 } from "path";
1416
+ import { promisify } from "util";
1417
+ var execFileAsync = promisify(execFile);
1418
+ var DEFAULT_OPENAI_MODEL = "gpt-4o";
1419
+ var DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514";
1420
+ var DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
1421
+ var DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com";
1422
+ var DEFAULT_MAX_TOKENS = 4096;
1423
+ var OpenAIProvider = class {
1424
+ apiKey;
1425
+ model;
1426
+ baseUrl;
1427
+ constructor(config = {}) {
1428
+ this.apiKey = config.apiKey?.trim() || process.env.OPENAI_API_KEY?.trim() || "";
1429
+ this.model = normalizeModel(config.model) ?? normalizeModel(process.env.TRAJECTORIES_LLM_MODEL) ?? DEFAULT_OPENAI_MODEL;
1430
+ this.baseUrl = config.baseUrl ?? process.env.OPENAI_BASE_URL ?? DEFAULT_OPENAI_BASE_URL;
1431
+ if (this.baseUrl !== DEFAULT_OPENAI_BASE_URL) {
1432
+ console.warn(
1433
+ `[trajectories] OpenAI base URL overridden to: ${this.baseUrl}`
1434
+ );
1435
+ }
1436
+ if (!this.apiKey) {
1437
+ throw new Error("OPENAI_API_KEY is required for OpenAIProvider");
1438
+ }
1439
+ }
1440
+ async complete(messages, options = {}) {
1441
+ const controller = new AbortController();
1442
+ const timeout = setTimeout(() => controller.abort(), 3e5);
1443
+ try {
1444
+ const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
1445
+ method: "POST",
1446
+ headers: {
1447
+ Authorization: `Bearer ${this.apiKey}`,
1448
+ "Content-Type": "application/json"
1449
+ },
1450
+ body: JSON.stringify({
1451
+ model: this.model,
1452
+ messages,
1453
+ max_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
1454
+ temperature: options.temperature ?? 0.2,
1455
+ response_format: options.jsonMode ? { type: "json_object" } : void 0
1456
+ }),
1457
+ signal: controller.signal
1458
+ });
1459
+ const body = await parseJson(response);
1460
+ if (!response.ok) {
1461
+ throw new Error(
1462
+ body.error?.message ?? `OpenAI request failed with status ${response.status}`
1463
+ );
1464
+ }
1465
+ const content = body.choices?.[0]?.message?.content;
1466
+ if (!content) {
1467
+ throw new Error("OpenAI response did not include completion content");
1468
+ }
1469
+ return content;
1470
+ } finally {
1471
+ clearTimeout(timeout);
1472
+ }
1473
+ }
1474
+ };
1475
+ var AnthropicProvider = class {
1476
+ apiKey;
1477
+ model;
1478
+ baseUrl;
1479
+ constructor(config = {}) {
1480
+ this.apiKey = config.apiKey?.trim() || process.env.ANTHROPIC_API_KEY?.trim() || "";
1481
+ this.model = normalizeModel(config.model) ?? normalizeModel(process.env.TRAJECTORIES_LLM_MODEL) ?? DEFAULT_ANTHROPIC_MODEL;
1482
+ this.baseUrl = config.baseUrl ?? process.env.ANTHROPIC_BASE_URL ?? DEFAULT_ANTHROPIC_BASE_URL;
1483
+ if (this.baseUrl !== DEFAULT_ANTHROPIC_BASE_URL) {
1484
+ console.warn(
1485
+ `[trajectories] Anthropic base URL overridden to: ${this.baseUrl}`
1486
+ );
1487
+ }
1488
+ if (!this.apiKey) {
1489
+ throw new Error("ANTHROPIC_API_KEY is required for AnthropicProvider");
1490
+ }
1491
+ }
1492
+ async complete(messages, options = {}) {
1493
+ const systemMessages = messages.filter((message) => message.role === "system").map((message) => message.content.trim()).filter(Boolean);
1494
+ const conversation = messages.filter((message) => message.role !== "system").map((message) => ({
1495
+ role: message.role,
1496
+ content: message.content
1497
+ }));
1498
+ if (conversation.length === 0) {
1499
+ throw new Error("AnthropicProvider requires at least one user message");
1500
+ }
1501
+ const controller = new AbortController();
1502
+ const timeout = setTimeout(() => controller.abort(), 3e5);
1503
+ try {
1504
+ const response = await fetch(`${this.baseUrl}/v1/messages`, {
1505
+ method: "POST",
1506
+ headers: {
1507
+ "Content-Type": "application/json",
1508
+ "anthropic-version": "2024-10-22",
1509
+ "x-api-key": this.apiKey
1510
+ },
1511
+ body: JSON.stringify({
1512
+ model: this.model,
1513
+ system: systemMessages.length > 0 ? systemMessages.join("\n\n") : void 0,
1514
+ messages: conversation,
1515
+ max_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
1516
+ temperature: options.temperature ?? 0.2
1517
+ }),
1518
+ signal: controller.signal
1519
+ });
1520
+ const body = await parseJson(response);
1521
+ if (!response.ok) {
1522
+ throw new Error(
1523
+ body.error?.message ?? `Anthropic request failed with status ${response.status}`
1524
+ );
1525
+ }
1526
+ const textBlocks = (body.content ?? []).filter(
1527
+ (block) => block.type === "text" && typeof block.text === "string"
1528
+ );
1529
+ const content = textBlocks.map((block) => block.text).join("\n").trim();
1530
+ if (!content) {
1531
+ throw new Error("Anthropic response did not include text content");
1532
+ }
1533
+ return content;
1534
+ } finally {
1535
+ clearTimeout(timeout);
1536
+ }
1537
+ }
1538
+ };
1539
+ var SUPPORTED_CLIS = ["claude", "codex", "gemini", "opencode"];
1540
+ var CLIProvider = class {
1541
+ cli;
1542
+ binaryPath;
1543
+ constructor(cli, binaryPath) {
1544
+ this.cli = cli;
1545
+ this.binaryPath = binaryPath;
1546
+ }
1547
+ get cliName() {
1548
+ return this.cli;
1549
+ }
1550
+ async complete(messages, _options = {}) {
1551
+ const prompt = messagesToPrompt(messages);
1552
+ const args = buildCliArgs(this.cli);
1553
+ const output = await spawnWithStdin(this.binaryPath, args, prompt);
1554
+ if (!output) {
1555
+ throw new Error(`${this.cli} CLI returned empty output`);
1556
+ }
1557
+ return output;
1558
+ }
1559
+ };
1560
+ function messagesToPrompt(messages) {
1561
+ const systemParts = [];
1562
+ const conversationParts = [];
1563
+ for (const msg of messages) {
1564
+ if (msg.role === "system") {
1565
+ systemParts.push(msg.content.trim());
1566
+ } else {
1567
+ conversationParts.push(msg.content.trim());
1568
+ }
1569
+ }
1570
+ const parts = [];
1571
+ if (systemParts.length > 0) {
1572
+ parts.push(systemParts.join("\n\n"));
1573
+ }
1574
+ if (conversationParts.length > 0) {
1575
+ parts.push(conversationParts.join("\n\n"));
1576
+ }
1577
+ return parts.join("\n\n---\n\n");
1578
+ }
1579
+ function buildCliArgs(cli) {
1580
+ switch (cli) {
1581
+ case "claude":
1582
+ return ["-p", "--output-format", "text"];
1583
+ case "codex":
1584
+ return ["exec", "-q"];
1585
+ case "gemini":
1586
+ return ["-p"];
1587
+ case "opencode":
1588
+ return ["run", "--no-color"];
1589
+ }
1590
+ }
1591
+ function spawnWithStdin(command, args, input) {
1592
+ return new Promise((resolve, reject) => {
1593
+ const child = spawn(command, args, {
1594
+ timeout: 3e5,
1595
+ stdio: ["pipe", "pipe", "pipe"]
1596
+ });
1597
+ const chunks = [];
1598
+ child.stdout.on("data", (chunk) => chunks.push(chunk));
1599
+ let stderr = "";
1600
+ child.stderr.on("data", (chunk) => {
1601
+ stderr += chunk.toString();
1602
+ });
1603
+ child.on("error", reject);
1604
+ child.on("close", (code) => {
1605
+ if (code !== 0) {
1606
+ reject(
1607
+ new Error(`CLI exited with code ${code}: ${stderr.slice(0, 200)}`)
1608
+ );
1609
+ } else {
1610
+ resolve(Buffer.concat(chunks).toString().trim());
1611
+ }
1612
+ });
1613
+ child.stdin.write(input);
1614
+ child.stdin.end();
1615
+ });
1616
+ }
1617
+ async function resolveProvider(config = {}) {
1618
+ const explicitProvider = (config.provider ?? process.env.TRAJECTORIES_LLM_PROVIDER)?.toLowerCase();
1619
+ const model = normalizeModel(config.model);
1620
+ if (explicitProvider === "openai") {
1621
+ return process.env.OPENAI_API_KEY ? new OpenAIProvider({ model }) : null;
1622
+ }
1623
+ if (explicitProvider === "anthropic") {
1624
+ return process.env.ANTHROPIC_API_KEY ? new AnthropicProvider({ model }) : null;
1625
+ }
1626
+ if (explicitProvider === "cli") {
1627
+ return resolveCLIProvider();
1628
+ }
1629
+ if (explicitProvider && explicitProvider !== "auto") {
1630
+ return null;
1631
+ }
1632
+ const cliProvider = await resolveCLIProvider();
1633
+ if (cliProvider) {
1634
+ return cliProvider;
1635
+ }
1636
+ if (process.env.OPENAI_API_KEY) {
1637
+ return new OpenAIProvider({ model });
1638
+ }
1639
+ if (process.env.ANTHROPIC_API_KEY) {
1640
+ return new AnthropicProvider({ model });
1641
+ }
1642
+ return null;
1643
+ }
1644
+ var CLI_SEARCH_PATHS = [
1645
+ "~/.local/bin",
1646
+ "~/.claude/local",
1647
+ "/usr/local/bin",
1648
+ "/opt/homebrew/bin"
1649
+ ];
1650
+ async function resolveCLIProvider() {
1651
+ const requestedCli = process.env.TRAJECTORIES_LLM_CLI?.trim().toLowerCase();
1652
+ const clisToTry = (() => {
1653
+ if (!requestedCli) {
1654
+ return SUPPORTED_CLIS;
1655
+ }
1656
+ if (SUPPORTED_CLIS.includes(requestedCli)) {
1657
+ return [requestedCli];
1658
+ }
1659
+ console.warn(
1660
+ `[trajectories] Unsupported TRAJECTORIES_LLM_CLI value "${requestedCli}", falling back to auto-detect`
1661
+ );
1662
+ return SUPPORTED_CLIS;
1663
+ })();
1664
+ for (const cli of clisToTry) {
1665
+ const path = await findBinary(cli);
1666
+ if (path) {
1667
+ return new CLIProvider(cli, path);
1668
+ }
1669
+ }
1670
+ return null;
1671
+ }
1672
+ async function findBinary(name) {
1673
+ try {
1674
+ const { stdout } = await execFileAsync("which", [name]);
1675
+ const path = stdout.trim();
1676
+ if (path) return path;
1677
+ } catch {
1678
+ }
1679
+ const home = homedir();
1680
+ for (const dir of CLI_SEARCH_PATHS) {
1681
+ const expanded = dir.startsWith("~/") ? join3(home, dir.slice(2)) : dir;
1682
+ const candidate = join3(expanded, name);
1683
+ try {
1684
+ accessSync(candidate, constants.X_OK);
1685
+ return candidate;
1686
+ } catch {
1687
+ }
1688
+ }
1689
+ return void 0;
1690
+ }
1691
+ function normalizeModel(value) {
1692
+ if (typeof value !== "string") {
1693
+ return void 0;
1694
+ }
1695
+ const trimmed = value.trim();
1696
+ return trimmed.length > 0 ? trimmed : void 0;
1697
+ }
1698
+ async function parseJson(response) {
1699
+ const text = await response.text();
1700
+ if (!text) {
1701
+ return {};
1702
+ }
1703
+ try {
1704
+ return JSON.parse(text);
1705
+ } catch {
1706
+ throw new Error(
1707
+ `Invalid JSON response (status ${response.status}, length ${text.length})`
1708
+ );
1709
+ }
1710
+ }
1711
+
1712
+ // src/compact/serializer.ts
1713
+ var DEFAULT_MAX_TOKENS2 = 3e4;
1714
+ var CHARS_PER_TOKEN = 4;
1715
+ var INCLUDED_SIGNIFICANCE = /* @__PURE__ */ new Set([
1716
+ "medium",
1717
+ "high",
1718
+ "critical"
1719
+ ]);
1720
+ function serializeForLLM(trajectories, maxTokens = DEFAULT_MAX_TOKENS2) {
1721
+ const maxChars = Math.max(0, maxTokens * CHARS_PER_TOKEN);
1722
+ const sessions = trajectories.map(renderSession);
1723
+ let document = joinSessions(sessions);
1724
+ if (document.length <= maxChars || sessions.length === 0) {
1725
+ return document;
1726
+ }
1727
+ const fixedChars = sessions.reduce(
1728
+ (total, session) => total + session.header.length + session.agents.length + session.decisions.length + session.findings.length + session.retrospective.length + session.filesAndCommits.length,
1729
+ 0
1730
+ );
1731
+ const chapterChars = sessions.reduce(
1732
+ (total, session) => total + session.chapters.reduce((sum, chapter) => sum + chapter.length, 0),
1733
+ 0
1734
+ );
1735
+ const remainingChapterChars = maxChars - fixedChars;
1736
+ if (remainingChapterChars <= 0 || chapterChars === 0) {
1737
+ return truncateText(document, maxChars);
1738
+ }
1739
+ const ratio = Math.min(1, remainingChapterChars / chapterChars);
1740
+ const truncatedSessions = sessions.map((session) => ({
1741
+ ...session,
1742
+ chapters: truncateChapters(
1743
+ session.chapters,
1744
+ session.chapters.reduce((sum, chapter) => sum + chapter.length, 0),
1745
+ ratio
1746
+ )
1747
+ }));
1748
+ document = joinSessions(truncatedSessions);
1749
+ return document.length <= maxChars ? document : truncateText(document, maxChars);
1750
+ }
1751
+ function renderSession(trajectory) {
1752
+ const sessionTitle = trajectory.task.title.trim() || trajectory.id;
1753
+ const duration = formatDuration(trajectory.startedAt, trajectory.completedAt);
1754
+ const header = [
1755
+ `## Session: ${sessionTitle} (${trajectory.status}, ${duration})`,
1756
+ trajectory.task.description ? `Description: ${trajectory.task.description}` : "",
1757
+ `Started: ${trajectory.startedAt}`,
1758
+ trajectory.completedAt ? `Completed: ${trajectory.completedAt}` : ""
1759
+ ].filter(Boolean).join("\n").concat("\n");
1760
+ const agents = trajectory.agents.length > 0 ? `Agents: ${trajectory.agents.map((agent) => `${agent.name} (${agent.role})`).join(", ")}
1761
+ ` : "Agents: none recorded\n";
1762
+ const chapters = trajectory.chapters.map(renderChapter);
1763
+ const decisions = renderDecisions(trajectory);
1764
+ const findings = renderFindings(trajectory);
1765
+ const retrospective = renderRetrospective(trajectory.retrospective);
1766
+ const filesAndCommits = [
1767
+ `Files changed: ${formatList(trajectory.filesChanged)}`,
1768
+ `Commits: ${formatList(trajectory.commits)}`
1769
+ ].join("\n").concat("\n");
1770
+ return {
1771
+ header,
1772
+ agents,
1773
+ chapters,
1774
+ decisions,
1775
+ findings,
1776
+ retrospective,
1777
+ filesAndCommits
1778
+ };
1779
+ }
1780
+ function renderChapter(chapter) {
1781
+ const lines = chapter.events.filter(shouldIncludeEvent).map((event) => formatEvent(event));
1782
+ const chapterBody = lines.length > 0 ? lines.map((line) => `- ${line}`).join("\n") : "- No medium/high/critical events captured";
1783
+ return [
1784
+ `### Chapter: ${chapter.title}`,
1785
+ `Agent: ${chapter.agentName}`,
1786
+ `Window: ${chapter.startedAt} -> ${chapter.endedAt ?? "ongoing"}`,
1787
+ chapterBody
1788
+ ].join("\n").concat("\n");
1789
+ }
1790
+ function renderDecisions(trajectory) {
1791
+ const seen = /* @__PURE__ */ new Set();
1792
+ const decisions = [];
1793
+ for (const chapter of trajectory.chapters) {
1794
+ for (const event of chapter.events) {
1795
+ if (event.type !== "decision") {
1796
+ continue;
1797
+ }
1798
+ const decision = asDecision(event.raw);
1799
+ if (!decision) {
1800
+ continue;
1801
+ }
1802
+ const key = `${decision.question}
1803
+ ${decision.chosen}
1804
+ ${decision.reasoning}`;
1805
+ if (!seen.has(key)) {
1806
+ seen.add(key);
1807
+ decisions.push(decision);
1808
+ }
1809
+ }
1810
+ }
1811
+ for (const decision of trajectory.retrospective?.decisions ?? []) {
1812
+ const key = `${decision.question}
1813
+ ${decision.chosen}
1814
+ ${decision.reasoning}`;
1815
+ if (!seen.has(key)) {
1816
+ seen.add(key);
1817
+ decisions.push(decision);
1818
+ }
1819
+ }
1820
+ if (decisions.length === 0) {
1821
+ return "Decisions:\n- None recorded\n";
1822
+ }
1823
+ return [
1824
+ "Decisions:",
1825
+ ...decisions.map(
1826
+ (decision) => [
1827
+ `- Question: ${decision.question}`,
1828
+ ` Chosen: ${decision.chosen}`,
1829
+ ` Reasoning: ${decision.reasoning}`
1830
+ ].join("\n")
1831
+ )
1832
+ ].join("\n").concat("\n");
1833
+ }
1834
+ function renderFindings(trajectory) {
1835
+ const findings = trajectory.chapters.flatMap(
1836
+ (chapter) => chapter.events.filter((event) => event.type === "finding").map((event) => asFinding(event.raw, event.content))
1837
+ );
1838
+ if (findings.length === 0) {
1839
+ return "Findings:\n- None recorded\n";
1840
+ }
1841
+ return [
1842
+ "Findings:",
1843
+ ...findings.map(
1844
+ (finding) => [
1845
+ `- What: ${finding.what}`,
1846
+ ` Where: ${finding.where}`,
1847
+ ` Significance: ${finding.significance}`
1848
+ ].join("\n")
1849
+ )
1850
+ ].join("\n").concat("\n");
1851
+ }
1852
+ function renderRetrospective(retrospective) {
1853
+ if (!retrospective) {
1854
+ return "Retrospective:\n- None recorded\n";
1855
+ }
1856
+ const lines = [
1857
+ "Retrospective:",
1858
+ `- Summary: ${retrospective.summary}`,
1859
+ ` Approach: ${retrospective.approach}`
1860
+ ];
1861
+ if (retrospective.challenges && retrospective.challenges.length > 0) {
1862
+ lines.push(` Challenges: ${retrospective.challenges.join("; ")}`);
1863
+ }
1864
+ if (retrospective.learnings && retrospective.learnings.length > 0) {
1865
+ lines.push(` Learnings: ${retrospective.learnings.join("; ")}`);
1866
+ }
1867
+ if (retrospective.suggestions && retrospective.suggestions.length > 0) {
1868
+ lines.push(` Suggestions: ${retrospective.suggestions.join("; ")}`);
1869
+ }
1870
+ if (retrospective.timeSpent) {
1871
+ lines.push(` Time spent: ${retrospective.timeSpent}`);
1872
+ }
1873
+ return lines.join("\n").concat("\n");
1874
+ }
1875
+ function shouldIncludeEvent(event) {
1876
+ if (event.type === "tool_call" || event.type === "tool_result") {
1877
+ return false;
1878
+ }
1879
+ return INCLUDED_SIGNIFICANCE.has(resolveSignificance(event));
1880
+ }
1881
+ function resolveSignificance(event) {
1882
+ if (event.significance) {
1883
+ return event.significance;
1884
+ }
1885
+ switch (event.type) {
1886
+ case "decision":
1887
+ case "finding":
1888
+ case "error":
1889
+ return "high";
1890
+ case "reflection":
1891
+ case "note":
1892
+ case "message_sent":
1893
+ case "message_received":
1894
+ return "medium";
1895
+ default:
1896
+ return "low";
1897
+ }
1898
+ }
1899
+ function formatEvent(event) {
1900
+ if (event.type === "decision") {
1901
+ const decision = asDecision(event.raw);
1902
+ if (decision) {
1903
+ return `[decision/${resolveSignificance(event)}] ${decision.question} -> ${decision.chosen}`;
1904
+ }
1905
+ }
1906
+ if (event.type === "finding") {
1907
+ const finding = asFinding(event.raw, event.content);
1908
+ return `[finding/${resolveSignificance(event)}] ${finding.what} @ ${finding.where}`;
1909
+ }
1910
+ return `[${event.type}/${resolveSignificance(event)}] ${event.content}`;
1911
+ }
1912
+ function asDecision(raw) {
1913
+ if (!raw || typeof raw !== "object") {
1914
+ return null;
1915
+ }
1916
+ const candidate = raw;
1917
+ if (typeof candidate.question !== "string" || typeof candidate.chosen !== "string" || typeof candidate.reasoning !== "string") {
1918
+ return null;
1919
+ }
1920
+ return {
1921
+ question: candidate.question,
1922
+ chosen: candidate.chosen,
1923
+ reasoning: candidate.reasoning,
1924
+ alternatives: Array.isArray(candidate.alternatives) ? candidate.alternatives : [],
1925
+ confidence: candidate.confidence
1926
+ };
1927
+ }
1928
+ function asFinding(raw, fallbackContent) {
1929
+ if (!raw || typeof raw !== "object") {
1930
+ return {
1931
+ what: fallbackContent,
1932
+ where: "unknown",
1933
+ significance: "Not structured",
1934
+ category: "other"
1935
+ };
1936
+ }
1937
+ const candidate = raw;
1938
+ return {
1939
+ what: typeof candidate.what === "string" && candidate.what.trim().length > 0 ? candidate.what : fallbackContent,
1940
+ where: typeof candidate.where === "string" && candidate.where.trim().length > 0 ? candidate.where : "unknown",
1941
+ significance: typeof candidate.significance === "string" && candidate.significance.trim().length > 0 ? candidate.significance : "Not structured",
1942
+ category: candidate.category ?? "other",
1943
+ suggestedAction: typeof candidate.suggestedAction === "string" ? candidate.suggestedAction : void 0,
1944
+ confidence: candidate.confidence
1945
+ };
1946
+ }
1947
+ function truncateChapters(chapters, totalChapterChars, ratio) {
1948
+ if (ratio >= 1 || totalChapterChars === 0) {
1949
+ return chapters;
1950
+ }
1951
+ let remaining = Math.floor(totalChapterChars * ratio);
1952
+ return chapters.map((chapter, index) => {
1953
+ if (remaining <= 0) {
1954
+ return "### Chapter: Truncated\n- Omitted due to token budget\n";
1955
+ }
1956
+ const proportionalTarget = index === chapters.length - 1 ? remaining : Math.floor(chapter.length * ratio);
1957
+ const allowance = Math.max(0, Math.min(chapter.length, proportionalTarget));
1958
+ remaining -= allowance;
1959
+ return truncateText(chapter, allowance);
1960
+ });
1961
+ }
1962
+ function joinSessions(sessions) {
1963
+ return sessions.map(
1964
+ (session) => [
1965
+ session.header,
1966
+ session.agents,
1967
+ ...session.chapters,
1968
+ session.decisions,
1969
+ session.findings,
1970
+ session.retrospective,
1971
+ session.filesAndCommits
1972
+ ].filter(Boolean).join("\n").trim()
1973
+ ).join("\n\n");
1974
+ }
1975
+ function truncateText(text, maxChars) {
1976
+ if (maxChars <= 0) {
1977
+ return "";
1978
+ }
1979
+ if (text.length <= maxChars) {
1980
+ return text;
1981
+ }
1982
+ if (maxChars <= 16) {
1983
+ return text.slice(0, maxChars);
1984
+ }
1985
+ return `${text.slice(0, maxChars - 16).trimEnd()}
1986
+ [truncated]
1987
+ `;
1988
+ }
1989
+ function formatList(values) {
1990
+ return values.length > 0 ? values.join(", ") : "none";
1991
+ }
1992
+ function formatDuration(startedAt, completedAt) {
1993
+ const start = new Date(startedAt).getTime();
1994
+ const end = new Date(completedAt ?? startedAt).getTime();
1995
+ const elapsedMs = Math.max(0, end - start);
1996
+ const minutes = Math.floor(elapsedMs / 6e4);
1997
+ const hours = Math.floor(minutes / 60);
1998
+ const remainingMinutes = minutes % 60;
1999
+ if (hours > 0 && remainingMinutes > 0) {
2000
+ return `${hours}h ${remainingMinutes}m`;
2001
+ }
2002
+ if (hours > 0) {
2003
+ return `${hours}h`;
2004
+ }
2005
+ if (minutes > 0) {
2006
+ return `${minutes}m`;
2007
+ }
2008
+ return completedAt ? "0m" : "ongoing";
2009
+ }
2010
+
2011
+ // src/cli/commands/compact.ts
886
2012
  function registerCompactCommand(program2) {
887
2013
  program2.command("compact").description(
888
2014
  "Compact trajectories into a summarized form (default: uncompacted only)"
@@ -892,16 +2018,22 @@ function registerCompactCommand(program2) {
892
2018
  ).option(
893
2019
  "--until <date>",
894
2020
  "Include trajectories until this date (ISO format)"
895
- ).option("--ids <ids>", "Comma-separated list of trajectory IDs to compact").option("--pr <number>", "Compact trajectories associated with a PR number").option(
2021
+ ).option("--ids <ids>", "Comma-separated list of trajectory IDs to compact").option(
2022
+ "--workflow <id>",
2023
+ "Compact trajectories with the specified workflow ID"
2024
+ ).option("--pr <number>", "Compact trajectories associated with a PR number").option(
896
2025
  "--branch <name>",
897
2026
  "Compact trajectories with commits not in the specified branch (e.g., main)"
898
2027
  ).option(
899
2028
  "--commits <shas>",
900
2029
  "Comma-separated commit SHAs to match trajectories against"
901
- ).option("--all", "Include all trajectories, even previously compacted ones").option("--dry-run", "Preview what would be compacted without saving").option("--output <path>", "Output path for compacted trajectory").action(async (options) => {
2030
+ ).option("--all", "Include all trajectories, even previously compacted ones").option("--llm", "Use LLM-based compaction when a provider is available").option("--no-llm", "Disable LLM-based compaction").option("--mechanical", "Force the original mechanical compaction flow").option(
2031
+ "--focus <areas>",
2032
+ "Comma-separated focus areas to emphasize in LLM compaction"
2033
+ ).option("--markdown", "Also write a Markdown companion file").option("--no-markdown", "Skip writing a Markdown companion file").option("--dry-run", "Preview what would be compacted without saving").option("--output <path>", "Output path for compacted trajectory").action(async (options) => {
902
2034
  const trajectories = await loadTrajectories(options);
903
2035
  if (trajectories.length === 0) {
904
- if (options.all || options.since || options.ids || options.pr || options.branch || options.commits) {
2036
+ if (options.all || options.since || options.ids || options.workflow || options.pr || options.branch || options.commits) {
905
2037
  console.log("No trajectories found matching criteria");
906
2038
  } else {
907
2039
  console.log(
@@ -912,17 +2044,92 @@ function registerCompactCommand(program2) {
912
2044
  }
913
2045
  console.log(`Compacting ${trajectories.length} trajectories...
914
2046
  `);
915
- const compacted = compactTrajectories(trajectories);
2047
+ const config = getCompactionConfig();
2048
+ const provider = await resolveProvider(config);
2049
+ const useLLM = shouldUseLLM(options, provider !== null);
2050
+ const markdownEnabled = options.markdown !== false;
2051
+ const mechanicalCompacted = compactTrajectories(
2052
+ trajectories,
2053
+ options.workflow
2054
+ );
2055
+ if (!useLLM || provider === null) {
2056
+ if (options.llm && provider === null && !options.mechanical) {
2057
+ console.log(
2058
+ "No LLM provider detected; falling back to mechanical compaction.\n"
2059
+ );
2060
+ }
2061
+ if (options.dryRun) {
2062
+ console.log("=== DRY RUN - Preview ===\n");
2063
+ printCompactedSummary(mechanicalCompacted);
2064
+ return;
2065
+ }
2066
+ const outputPath2 = options.output || getDefaultOutputPath(mechanicalCompacted, options.workflow);
2067
+ saveCompactionArtifacts(
2068
+ mechanicalCompacted,
2069
+ outputPath2,
2070
+ markdownEnabled
2071
+ );
2072
+ await markTrajectoriesAsCompacted(trajectories, mechanicalCompacted.id);
2073
+ console.log(`
2074
+ Compacted trajectory saved to: ${outputPath2}`);
2075
+ if (markdownEnabled) {
2076
+ console.log(
2077
+ `Markdown summary saved to: ${getMarkdownOutputPath(outputPath2)}`
2078
+ );
2079
+ }
2080
+ printCompactedSummary(mechanicalCompacted);
2081
+ return;
2082
+ }
2083
+ const llmPlan = buildLLMCompactionPlan(
2084
+ trajectories,
2085
+ parseFocusAreas(options.focus),
2086
+ config.maxInputTokens,
2087
+ config.maxOutputTokens
2088
+ );
2089
+ console.log(
2090
+ `Using ${getProviderLabel(provider)} compaction${config.model ? ` with model ${config.model}` : ""}.`
2091
+ );
2092
+ console.log(
2093
+ `Estimated: ~${llmPlan.estimatedInputTokens} input tokens, ~${llmPlan.estimatedOutputTokens} output tokens`
2094
+ );
916
2095
  if (options.dryRun) {
917
- console.log("=== DRY RUN - Preview ===\n");
918
- printCompactedSummary(compacted);
2096
+ printLLMDryRun(llmPlan, config.model, options.workflow);
919
2097
  return;
920
2098
  }
921
- const outputPath = options.output || getDefaultOutputPath(compacted);
922
- saveCompactedTrajectory(compacted, outputPath);
2099
+ const llmOutput = await provider.complete(llmPlan.messages, {
2100
+ maxTokens: config.maxOutputTokens,
2101
+ temperature: config.temperature,
2102
+ jsonMode: provider instanceof OpenAIProvider
2103
+ });
2104
+ const llmCompacted = parseCompactionResponse(llmOutput);
2105
+ const mergedCompaction = mergeCompactionWithMetadata(
2106
+ {
2107
+ id: mechanicalCompacted.id,
2108
+ version: mechanicalCompacted.version,
2109
+ type: mechanicalCompacted.type,
2110
+ compactedAt: mechanicalCompacted.compactedAt,
2111
+ sourceTrajectories: mechanicalCompacted.sourceTrajectories,
2112
+ dateRange: mechanicalCompacted.dateRange,
2113
+ summary: mechanicalCompacted.summary,
2114
+ filesAffected: mechanicalCompacted.filesAffected,
2115
+ commits: mechanicalCompacted.commits
2116
+ },
2117
+ llmCompacted
2118
+ );
2119
+ const compacted = {
2120
+ ...mechanicalCompacted,
2121
+ ...mergedCompaction
2122
+ };
2123
+ const outputPath = options.output || getDefaultOutputPath(compacted, options.workflow);
2124
+ saveCompactionArtifacts(compacted, outputPath, markdownEnabled);
923
2125
  await markTrajectoriesAsCompacted(trajectories, compacted.id);
924
2126
  console.log(`
925
2127
  Compacted trajectory saved to: ${outputPath}`);
2128
+ if (markdownEnabled) {
2129
+ console.log(
2130
+ `Markdown summary saved to: ${getMarkdownOutputPath(outputPath)}`
2131
+ );
2132
+ }
926
2133
  printCompactedSummary(compacted);
927
2134
  });
928
2135
  }
@@ -943,55 +2150,55 @@ async function loadTrajectories(options) {
943
2150
  const searchPaths = getSearchPaths();
944
2151
  const seenIds = /* @__PURE__ */ new Set();
945
2152
  for (const searchPath of searchPaths) {
946
- if (!existsSync2(searchPath)) continue;
2153
+ if (!existsSync3(searchPath)) continue;
947
2154
  const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
948
2155
  process.env.TRAJECTORIES_DATA_DIR = searchPath;
949
- try {
950
- const storage = new FileStorage();
951
- await storage.initialize();
952
- const summaries = await storage.list({
953
- status: "completed",
954
- limit: Number.MAX_SAFE_INTEGER
955
- });
956
- for (const summary of summaries) {
957
- if (seenIds.has(summary.id)) continue;
958
- if (compactedIds.has(summary.id)) continue;
959
- if (targetIds && !targetIds.includes(summary.id)) continue;
960
- const startDate = new Date(summary.startedAt);
961
- if (sinceDate && startDate < sinceDate) continue;
962
- if (untilDate && startDate > untilDate) continue;
963
- const trajectory = await storage.get(summary.id);
964
- if (trajectory) {
965
- seenIds.add(summary.id);
966
- if (options.pr) {
967
- const escaped = options.pr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
968
- const prPattern = new RegExp(
969
- `#${escaped}\\b|\\bPR\\s*#?\\s*${escaped}\\b`,
970
- "i"
971
- );
972
- const matchesPR = prPattern.test(trajectory.task.title) || prPattern.test(trajectory.task.description || "") || trajectory.commits.some((c) => prPattern.test(c));
973
- if (!matchesPR) continue;
974
- }
975
- if (branchCommits) {
976
- const hasMatchingCommit = trajectory.commits.some(
977
- (c) => branchCommits.has(c.slice(0, 7)) || branchCommits.has(c)
978
- );
979
- if (!hasMatchingCommit && trajectory.commits.length > 0) continue;
980
- }
981
- if (targetCommits) {
982
- const hasMatchingCommit = trajectory.commits.some(
983
- (c) => targetCommits.has(c) || targetCommits.has(c.slice(0, 7))
984
- );
985
- if (!hasMatchingCommit) continue;
986
- }
987
- trajectories.push(trajectory);
2156
+ const storage = new FileStorage();
2157
+ if (originalDataDir !== void 0) {
2158
+ process.env.TRAJECTORIES_DATA_DIR = originalDataDir;
2159
+ } else {
2160
+ delete process.env.TRAJECTORIES_DATA_DIR;
2161
+ }
2162
+ await storage.initialize();
2163
+ const summaries = await storage.list({
2164
+ status: "completed",
2165
+ limit: Number.MAX_SAFE_INTEGER
2166
+ });
2167
+ for (const summary of summaries) {
2168
+ if (seenIds.has(summary.id)) continue;
2169
+ if (compactedIds.has(summary.id)) continue;
2170
+ if (targetIds && !targetIds.includes(summary.id)) continue;
2171
+ const startDate = new Date(summary.startedAt);
2172
+ if (sinceDate && startDate < sinceDate) continue;
2173
+ if (untilDate && startDate > untilDate) continue;
2174
+ const trajectory = await storage.get(summary.id);
2175
+ if (trajectory) {
2176
+ seenIds.add(summary.id);
2177
+ if (options.workflow && trajectory.workflowId !== options.workflow) {
2178
+ continue;
988
2179
  }
989
- }
990
- } finally {
991
- if (originalDataDir !== void 0) {
992
- process.env.TRAJECTORIES_DATA_DIR = originalDataDir;
993
- } else {
994
- delete process.env.TRAJECTORIES_DATA_DIR;
2180
+ if (options.pr) {
2181
+ const escaped = options.pr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2182
+ const prPattern = new RegExp(
2183
+ `#${escaped}\\b|\\bPR\\s*#?\\s*${escaped}\\b`,
2184
+ "i"
2185
+ );
2186
+ const matchesPR = prPattern.test(trajectory.task.title) || prPattern.test(trajectory.task.description || "") || trajectory.commits.some((c) => prPattern.test(c));
2187
+ if (!matchesPR) continue;
2188
+ }
2189
+ if (branchCommits) {
2190
+ const hasMatchingCommit = trajectory.commits.some(
2191
+ (c) => branchCommits.has(c.slice(0, 7)) || branchCommits.has(c)
2192
+ );
2193
+ if (!hasMatchingCommit && trajectory.commits.length > 0) continue;
2194
+ }
2195
+ if (targetCommits) {
2196
+ const hasMatchingCommit = trajectory.commits.some(
2197
+ (c) => targetCommits.has(c) || targetCommits.has(c.slice(0, 7))
2198
+ );
2199
+ if (!hasMatchingCommit) continue;
2200
+ }
2201
+ trajectories.push(trajectory);
995
2202
  }
996
2203
  }
997
2204
  }
@@ -1000,8 +2207,9 @@ async function loadTrajectories(options) {
1000
2207
  function getBranchCommits(targetBranch) {
1001
2208
  const commits = /* @__PURE__ */ new Set();
1002
2209
  try {
1003
- const output = execSync(
1004
- `git log '${targetBranch.replace(/'/g, "'\\''")}'..HEAD --format=%H`,
2210
+ const output = execFileSync(
2211
+ "git",
2212
+ ["log", `${targetBranch}..HEAD`, "--format=%H"],
1005
2213
  {
1006
2214
  encoding: "utf-8",
1007
2215
  stdio: ["pipe", "pipe", "pipe"]
@@ -1024,10 +2232,10 @@ function getCompactedTrajectoryIds() {
1024
2232
  const compacted = /* @__PURE__ */ new Set();
1025
2233
  const searchPaths = getSearchPaths();
1026
2234
  for (const searchPath of searchPaths) {
1027
- const indexPath = join2(searchPath, "index.json");
1028
- if (!existsSync2(indexPath)) continue;
2235
+ const indexPath = join4(searchPath, "index.json");
2236
+ if (!existsSync3(indexPath)) continue;
1029
2237
  try {
1030
- const indexContent = readFileSync(indexPath, "utf-8");
2238
+ const indexContent = readFileSync2(indexPath, "utf-8");
1031
2239
  const index = JSON.parse(indexContent);
1032
2240
  for (const [id, entry] of Object.entries(index.trajectories || {})) {
1033
2241
  if (entry.compactedInto) {
@@ -1042,10 +2250,10 @@ function getCompactedTrajectoryIds() {
1042
2250
  async function markTrajectoriesAsCompacted(trajectories, compactedIntoId) {
1043
2251
  const searchPaths = getSearchPaths();
1044
2252
  for (const searchPath of searchPaths) {
1045
- const indexPath = join2(searchPath, "index.json");
1046
- if (!existsSync2(indexPath)) continue;
2253
+ const indexPath = join4(searchPath, "index.json");
2254
+ if (!existsSync3(indexPath)) continue;
1047
2255
  try {
1048
- const indexContent = readFileSync(indexPath, "utf-8");
2256
+ const indexContent = readFileSync2(indexPath, "utf-8");
1049
2257
  const index = JSON.parse(indexContent);
1050
2258
  let updated = false;
1051
2259
  for (const traj of trajectories) {
@@ -1081,7 +2289,7 @@ function parseRelativeDate(input) {
1081
2289
  }
1082
2290
  return new Date(input);
1083
2291
  }
1084
- function compactTrajectories(trajectories) {
2292
+ function compactTrajectories(trajectories, workflowId) {
1085
2293
  const allDecisions = [];
1086
2294
  const allLearnings = [];
1087
2295
  const allFindings = [];
@@ -1144,6 +2352,7 @@ function compactTrajectories(trajectories) {
1144
2352
  version: 1,
1145
2353
  type: "compacted",
1146
2354
  compactedAt: (/* @__PURE__ */ new Date()).toISOString(),
2355
+ workflowId,
1147
2356
  sourceTrajectories: trajectories.map((t) => t.id),
1148
2357
  dateRange: {
1149
2358
  start: minDate.toISOString(),
@@ -1214,56 +2423,206 @@ function groupDecisions(decisions) {
1214
2423
  (a, b) => b.decisions.length - a.decisions.length
1215
2424
  );
1216
2425
  }
1217
- function getDefaultOutputPath(compacted) {
2426
+ function shouldUseLLM(options, providerAvailable) {
2427
+ if (options.mechanical) {
2428
+ return false;
2429
+ }
2430
+ if (options.llm === false) {
2431
+ return false;
2432
+ }
2433
+ if (options.llm === true) {
2434
+ return providerAvailable;
2435
+ }
2436
+ return providerAvailable;
2437
+ }
2438
+ function buildLLMCompactionPlan(trajectories, focusAreas, maxInputTokens, maxOutputTokens) {
2439
+ const serialized = serializeForLLM(trajectories, maxInputTokens);
2440
+ const messages = buildCompactionPrompt(serialized, {
2441
+ focusAreas,
2442
+ maxOutputTokens
2443
+ });
2444
+ return {
2445
+ messages,
2446
+ estimatedInputTokens: estimateTokens(
2447
+ messages.map((message) => message.content).join("\n\n")
2448
+ ),
2449
+ estimatedOutputTokens: maxOutputTokens,
2450
+ focusAreas
2451
+ };
2452
+ }
2453
+ function parseFocusAreas(focus) {
2454
+ if (!focus) {
2455
+ return [];
2456
+ }
2457
+ return focus.split(",").map((area) => area.trim()).filter(Boolean);
2458
+ }
2459
+ function estimateTokens(text) {
2460
+ return Math.max(1, Math.ceil(text.length / 4));
2461
+ }
2462
+ function printLLMDryRun(plan, model, workflowId) {
2463
+ console.log("=== DRY RUN - LLM Prompt Preview ===\n");
2464
+ console.log(
2465
+ `Estimated: ~${plan.estimatedInputTokens} input tokens, ~${plan.estimatedOutputTokens} output tokens`
2466
+ );
2467
+ if (model) {
2468
+ console.log(`Configured model: ${model}`);
2469
+ }
2470
+ if (workflowId) {
2471
+ console.log(`Workflow: ${workflowId}`);
2472
+ }
2473
+ if (plan.focusAreas.length > 0) {
2474
+ console.log(`Focus: ${plan.focusAreas.join(", ")}`);
2475
+ }
2476
+ console.log("");
2477
+ for (const message of plan.messages) {
2478
+ console.log(`[${message.role.toUpperCase()}]`);
2479
+ console.log(message.content);
2480
+ console.log("");
2481
+ }
2482
+ }
2483
+ function getProviderLabel(provider) {
2484
+ if (provider instanceof OpenAIProvider) {
2485
+ return "OpenAI";
2486
+ }
2487
+ if (provider instanceof AnthropicProvider) {
2488
+ return "Anthropic";
2489
+ }
2490
+ if (provider instanceof CLIProvider) {
2491
+ return `CLI (${provider.cliName})`;
2492
+ }
2493
+ return "LLM";
2494
+ }
2495
+ function getDefaultOutputPath(compacted, workflowId) {
1218
2496
  const trajDir = process.env.TRAJECTORIES_DATA_DIR || ".trajectories";
1219
- const compactedDir = join2(trajDir, "compacted");
1220
- if (!existsSync2(compactedDir)) {
2497
+ const compactedDir = join4(trajDir, "compacted");
2498
+ if (!existsSync3(compactedDir)) {
1221
2499
  mkdirSync(compactedDir, { recursive: true });
1222
2500
  }
2501
+ if (workflowId) {
2502
+ return join4(compactedDir, `workflow-${workflowId}.json`);
2503
+ }
1223
2504
  const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1224
- return join2(compactedDir, `${compacted.id}_${dateStr}.json`);
2505
+ return join4(compactedDir, `${compacted.id}_${dateStr}.json`);
1225
2506
  }
1226
- function saveCompactedTrajectory(compacted, outputPath) {
1227
- const dir = join2(outputPath, "..");
1228
- if (!existsSync2(dir)) {
2507
+ function saveCompactionArtifacts(compacted, outputPath, markdownEnabled) {
2508
+ const dir = dirname(outputPath);
2509
+ if (!existsSync3(dir)) {
1229
2510
  mkdirSync(dir, { recursive: true });
1230
2511
  }
1231
2512
  writeFileSync(outputPath, JSON.stringify(compacted, null, 2));
2513
+ if (markdownEnabled) {
2514
+ writeFileSync(
2515
+ getMarkdownOutputPath(outputPath),
2516
+ renderCompactionMarkdown(compacted)
2517
+ );
2518
+ }
2519
+ }
2520
+ function getMarkdownOutputPath(outputPath) {
2521
+ return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".md") : `${outputPath}.md`;
2522
+ }
2523
+ function renderCompactionMarkdown(compacted) {
2524
+ if (compacted.narrative) {
2525
+ return generateCompactionMarkdown(
2526
+ compacted
2527
+ );
2528
+ }
2529
+ const decisionGroups = compacted.decisionGroups.length > 0 ? compacted.decisionGroups.map((group) => {
2530
+ const decisions = group.decisions.length > 0 ? group.decisions.map(
2531
+ (decision) => `- ${decision.question} -> ${decision.chosen} (${decision.fromTrajectory})`
2532
+ ).join("\n") : "- None";
2533
+ return `## ${capitalize(group.category)}
2534
+ ${decisions}`;
2535
+ }).join("\n\n") : "## Decision Groups\n- None";
2536
+ const learnings = compacted.keyLearnings.length > 0 ? compacted.keyLearnings.map((learning) => `- ${learning}`).join("\n") : "- None";
2537
+ const findings = compacted.keyFindings.length > 0 ? compacted.keyFindings.map((finding) => `- ${finding}`).join("\n") : "- None";
2538
+ return [
2539
+ `# Trajectory Compaction: ${formatDate3(compacted.dateRange.start)} - ${formatDate3(compacted.dateRange.end)}`,
2540
+ "",
2541
+ "## Summary",
2542
+ `- Sessions: ${compacted.sourceTrajectories.length}`,
2543
+ ...compacted.workflowId ? [`- Workflow: ${compacted.workflowId}`] : [],
2544
+ `- Decisions: ${compacted.summary.totalDecisions}`,
2545
+ `- Events: ${compacted.summary.totalEvents}`,
2546
+ `- Agents: ${compacted.summary.uniqueAgents.join(", ") || "None"}`,
2547
+ `- Files: ${compacted.filesAffected.length}`,
2548
+ `- Commits: ${compacted.commits.length}`,
2549
+ "",
2550
+ decisionGroups,
2551
+ "",
2552
+ "## Key Learnings",
2553
+ learnings,
2554
+ "",
2555
+ "## Key Findings",
2556
+ findings
2557
+ ].join("\n");
1232
2558
  }
1233
2559
  function printCompactedSummary(compacted) {
1234
2560
  console.log("=== Compacted Trajectory Summary ===\n");
1235
2561
  console.log(`ID: ${compacted.id}`);
2562
+ if (compacted.workflowId) {
2563
+ console.log(`Workflow: ${compacted.workflowId}`);
2564
+ }
1236
2565
  console.log(`Source trajectories: ${compacted.sourceTrajectories.length}`);
1237
2566
  console.log(
1238
- `Date range: ${formatDate2(compacted.dateRange.start)} - ${formatDate2(compacted.dateRange.end)}`
2567
+ `Date range: ${formatDate3(compacted.dateRange.start)} - ${formatDate3(compacted.dateRange.end)}`
1239
2568
  );
1240
2569
  console.log(`Total decisions: ${compacted.summary.totalDecisions}`);
1241
2570
  console.log(`Total events: ${compacted.summary.totalEvents}`);
1242
2571
  console.log(`Agents: ${compacted.summary.uniqueAgents.join(", ")}`);
1243
2572
  console.log("");
1244
- console.log("=== Decision Groups ===\n");
1245
- for (const group of compacted.decisionGroups) {
1246
- console.log(
1247
- `${capitalize(group.category)} (${group.decisions.length} decisions):`
1248
- );
1249
- for (const decision of group.decisions.slice(0, 3)) {
1250
- console.log(` - ${decision.question}`);
1251
- console.log(` Chose: ${decision.chosen}`);
2573
+ if (compacted.narrative) {
2574
+ console.log("=== Narrative ===\n");
2575
+ console.log(compacted.narrative);
2576
+ console.log("");
2577
+ if (compacted.decisions && compacted.decisions.length > 0) {
2578
+ console.log("=== Key Decisions ===\n");
2579
+ for (const decision of compacted.decisions.slice(0, 5)) {
2580
+ console.log(` - ${decision.question}`);
2581
+ console.log(` Chosen: ${decision.chosen}`);
2582
+ if (decision.impact) {
2583
+ console.log(` Impact: ${decision.impact}`);
2584
+ }
2585
+ }
2586
+ if (compacted.decisions.length > 5) {
2587
+ console.log(` ... and ${compacted.decisions.length - 5} more`);
2588
+ }
2589
+ console.log("");
1252
2590
  }
1253
- if (group.decisions.length > 3) {
1254
- console.log(` ... and ${group.decisions.length - 3} more`);
2591
+ if (compacted.openQuestions && compacted.openQuestions.length > 0) {
2592
+ console.log("=== Open Questions ===\n");
2593
+ for (const question of compacted.openQuestions.slice(0, 5)) {
2594
+ console.log(` - ${question}`);
2595
+ }
2596
+ if (compacted.openQuestions.length > 5) {
2597
+ console.log(` ... and ${compacted.openQuestions.length - 5} more`);
2598
+ }
2599
+ console.log("");
1255
2600
  }
1256
- console.log("");
1257
- }
1258
- if (compacted.keyLearnings.length > 0) {
1259
- console.log("=== Key Learnings ===\n");
1260
- for (const learning of compacted.keyLearnings.slice(0, 5)) {
1261
- console.log(` - ${learning}`);
2601
+ } else {
2602
+ console.log("=== Decision Groups ===\n");
2603
+ for (const group of compacted.decisionGroups) {
2604
+ console.log(
2605
+ `${capitalize(group.category)} (${group.decisions.length} decisions):`
2606
+ );
2607
+ for (const decision of group.decisions.slice(0, 3)) {
2608
+ console.log(` - ${decision.question}`);
2609
+ console.log(` Chose: ${decision.chosen}`);
2610
+ }
2611
+ if (group.decisions.length > 3) {
2612
+ console.log(` ... and ${group.decisions.length - 3} more`);
2613
+ }
2614
+ console.log("");
1262
2615
  }
1263
- if (compacted.keyLearnings.length > 5) {
1264
- console.log(` ... and ${compacted.keyLearnings.length - 5} more`);
2616
+ if (compacted.keyLearnings.length > 0) {
2617
+ console.log("=== Key Learnings ===\n");
2618
+ for (const learning of compacted.keyLearnings.slice(0, 5)) {
2619
+ console.log(` - ${learning}`);
2620
+ }
2621
+ if (compacted.keyLearnings.length > 5) {
2622
+ console.log(` ... and ${compacted.keyLearnings.length - 5} more`);
2623
+ }
2624
+ console.log("");
1265
2625
  }
1266
- console.log("");
1267
2626
  }
1268
2627
  if (compacted.filesAffected.length > 0) {
1269
2628
  console.log(`Files affected: ${compacted.filesAffected.length}`);
@@ -1272,7 +2631,7 @@ function printCompactedSummary(compacted) {
1272
2631
  console.log(`Commits: ${compacted.commits.length}`);
1273
2632
  }
1274
2633
  }
1275
- function formatDate2(isoString) {
2634
+ function formatDate3(isoString) {
1276
2635
  return new Date(isoString).toLocaleDateString("en-US", {
1277
2636
  month: "short",
1278
2637
  day: "numeric",
@@ -1284,12 +2643,12 @@ function capitalize(str) {
1284
2643
  }
1285
2644
 
1286
2645
  // src/cli/commands/complete.ts
1287
- import { existsSync as existsSync3 } from "fs";
2646
+ import { existsSync as existsSync4 } from "fs";
1288
2647
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
1289
- import { join as join3 } from "path";
2648
+ import { join as join5 } from "path";
1290
2649
 
1291
2650
  // src/core/trace.ts
1292
- import { execSync as execSync2 } from "child_process";
2651
+ import { execSync } from "child_process";
1293
2652
  import { createHash } from "crypto";
1294
2653
  function isValidGitRef(ref) {
1295
2654
  const validRefPattern = /^[a-zA-Z0-9_\-./]+$/;
@@ -1309,7 +2668,7 @@ function isValidGitRef(ref) {
1309
2668
  }
1310
2669
  function isGitRepo() {
1311
2670
  try {
1312
- execSync2("git rev-parse --is-inside-work-tree", {
2671
+ execSync("git rev-parse --is-inside-work-tree", {
1313
2672
  encoding: "utf-8",
1314
2673
  stdio: ["pipe", "pipe", "pipe"]
1315
2674
  });
@@ -1323,7 +2682,7 @@ function getGitHead() {
1323
2682
  return null;
1324
2683
  }
1325
2684
  try {
1326
- const head = execSync2("git rev-parse HEAD", {
2685
+ const head = execSync("git rev-parse HEAD", {
1327
2686
  encoding: "utf-8",
1328
2687
  stdio: ["pipe", "pipe", "pipe"]
1329
2688
  }).trim();
@@ -1375,7 +2734,7 @@ function getChangedFiles(startRef, endRef = "HEAD") {
1375
2734
  return [];
1376
2735
  }
1377
2736
  try {
1378
- const diffOutput = execSync2(`git diff ${startRef}..${endRef}`, {
2737
+ const diffOutput = execSync(`git diff ${startRef}..${endRef}`, {
1379
2738
  encoding: "utf-8",
1380
2739
  stdio: ["pipe", "pipe", "pipe"],
1381
2740
  maxBuffer: 10 * 1024 * 1024
@@ -1480,8 +2839,8 @@ function createTraceRef(startRef, traceId) {
1480
2839
  }
1481
2840
 
1482
2841
  // src/core/trailers.ts
1483
- import { execSync as execSync3 } from "child_process";
1484
- import { readFileSync as readFileSync2 } from "fs";
2842
+ import { execSync as execSync2 } from "child_process";
2843
+ import { readFileSync as readFileSync3 } from "fs";
1485
2844
  function getCommitsBetween(startRef, endRef = "HEAD") {
1486
2845
  if (!isGitRepo()) {
1487
2846
  return [];
@@ -1490,7 +2849,7 @@ function getCommitsBetween(startRef, endRef = "HEAD") {
1490
2849
  return [];
1491
2850
  }
1492
2851
  try {
1493
- const output = execSync3(
2852
+ const output = execSync2(
1494
2853
  `git log --format=%H%n%h%n%s%n%an%n%aI%n--- ${startRef}..${endRef}`,
1495
2854
  {
1496
2855
  encoding: "utf-8",
@@ -1527,7 +2886,7 @@ function getFilesChangedBetween(startRef, endRef = "HEAD") {
1527
2886
  return [];
1528
2887
  }
1529
2888
  try {
1530
- const output = execSync3(`git diff --name-only ${startRef}..${endRef}`, {
2889
+ const output = execSync2(`git diff --name-only ${startRef}..${endRef}`, {
1531
2890
  encoding: "utf-8",
1532
2891
  stdio: ["pipe", "pipe", "pipe"]
1533
2892
  });
@@ -1585,13 +2944,13 @@ function detectExistingHook() {
1585
2944
  return "none";
1586
2945
  }
1587
2946
  try {
1588
- const hooksDir = execSync3("git rev-parse --git-dir", {
2947
+ const hooksDir = execSync2("git rev-parse --git-dir", {
1589
2948
  encoding: "utf-8",
1590
2949
  stdio: ["pipe", "pipe", "pipe"]
1591
2950
  }).trim();
1592
2951
  const hookPath = `${hooksDir}/hooks/prepare-commit-msg`;
1593
2952
  try {
1594
- const content = readFileSync2(hookPath, "utf-8");
2953
+ const content = readFileSync3(hookPath, "utf-8");
1595
2954
  if (content.includes("agent-trajectories")) {
1596
2955
  return "ours";
1597
2956
  }
@@ -1607,17 +2966,17 @@ function detectExistingHook() {
1607
2966
  // src/cli/commands/complete.ts
1608
2967
  async function saveTraceFile(trajectory, trace) {
1609
2968
  const dataDir = process.env.TRAJECTORIES_DATA_DIR;
1610
- const baseDir = dataDir ? dataDir : join3(process.cwd(), ".trajectories");
1611
- const completedDir = join3(baseDir, "completed");
2969
+ const baseDir = dataDir ? dataDir : join5(process.cwd(), ".trajectories");
2970
+ const completedDir = join5(baseDir, "completed");
1612
2971
  const date = new Date(trajectory.completedAt ?? trajectory.startedAt);
1613
- const monthDir = join3(
2972
+ const monthDir = join5(
1614
2973
  completedDir,
1615
2974
  `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`
1616
2975
  );
1617
- if (!existsSync3(monthDir)) {
2976
+ if (!existsSync4(monthDir)) {
1618
2977
  await mkdir2(monthDir, { recursive: true });
1619
2978
  }
1620
- const tracePath = join3(monthDir, `${trajectory.id}.trace.json`);
2979
+ const tracePath = join5(monthDir, `${trajectory.id}.trace.json`);
1621
2980
  await writeFile2(tracePath, JSON.stringify(trace, null, 2), "utf-8");
1622
2981
  }
1623
2982
  function registerCompleteCommand(program2) {
@@ -1730,17 +3089,17 @@ function registerDecisionCommand(program2) {
1730
3089
  }
1731
3090
 
1732
3091
  // src/cli/commands/enable.ts
1733
- import { execSync as execSync4 } from "child_process";
1734
- import { existsSync as existsSync4 } from "fs";
3092
+ import { execSync as execSync3 } from "child_process";
3093
+ import { existsSync as existsSync5 } from "fs";
1735
3094
  import { chmod, mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
1736
- import { join as join4 } from "path";
3095
+ import { join as join6 } from "path";
1737
3096
  function getHooksDir() {
1738
3097
  try {
1739
- const gitDir = execSync4("git rev-parse --git-dir", {
3098
+ const gitDir = execSync3("git rev-parse --git-dir", {
1740
3099
  encoding: "utf-8",
1741
3100
  stdio: ["pipe", "pipe", "pipe"]
1742
3101
  }).trim();
1743
- return join4(gitDir, "hooks");
3102
+ return join6(gitDir, "hooks");
1744
3103
  } catch {
1745
3104
  return null;
1746
3105
  }
@@ -1758,7 +3117,7 @@ function registerEnableCommand(program2) {
1758
3117
  console.error("Error: Could not determine git hooks directory");
1759
3118
  throw new Error("Cannot find hooks directory");
1760
3119
  }
1761
- const hookPath = join4(hooksDir, "prepare-commit-msg");
3120
+ const hookPath = join6(hooksDir, "prepare-commit-msg");
1762
3121
  const existing = detectExistingHook();
1763
3122
  if (existing === "other" && !options.force) {
1764
3123
  console.error("Error: A prepare-commit-msg hook already exists");
@@ -1771,7 +3130,7 @@ function registerEnableCommand(program2) {
1771
3130
  console.log("Trajectory hook is already installed");
1772
3131
  return;
1773
3132
  }
1774
- if (!existsSync4(hooksDir)) {
3133
+ if (!existsSync5(hooksDir)) {
1775
3134
  await mkdir3(hooksDir, { recursive: true });
1776
3135
  }
1777
3136
  const hookContent = generateHookScript();
@@ -1793,7 +3152,7 @@ function registerEnableCommand(program2) {
1793
3152
  console.error("Error: Could not determine git hooks directory");
1794
3153
  throw new Error("Cannot find hooks directory");
1795
3154
  }
1796
- const hookPath = join4(hooksDir, "prepare-commit-msg");
3155
+ const hookPath = join6(hooksDir, "prepare-commit-msg");
1797
3156
  const existing = detectExistingHook();
1798
3157
  if (existing === "none") {
1799
3158
  console.log("No trajectory hook installed");
@@ -1815,7 +3174,7 @@ function registerEnableCommand(program2) {
1815
3174
  // src/cli/commands/export.ts
1816
3175
  import { exec } from "child_process";
1817
3176
  import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
1818
- import { join as join5 } from "path";
3177
+ import { join as join7 } from "path";
1819
3178
 
1820
3179
  // src/export/json.ts
1821
3180
  function exportToJSON(trajectory, options) {
@@ -2229,7 +3588,7 @@ h2 {
2229
3588
  function escapeHtml(text) {
2230
3589
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
2231
3590
  }
2232
- function formatDate3(isoDate) {
3591
+ function formatDate4(isoDate) {
2233
3592
  const date = new Date(isoDate);
2234
3593
  return date.toLocaleDateString("en-US", {
2235
3594
  month: "short",
@@ -2239,7 +3598,7 @@ function formatDate3(isoDate) {
2239
3598
  minute: "2-digit"
2240
3599
  });
2241
3600
  }
2242
- function formatDuration(startDate, endDate) {
3601
+ function formatDuration2(startDate, endDate) {
2243
3602
  const start = new Date(startDate).getTime();
2244
3603
  const end = endDate ? new Date(endDate).getTime() : Date.now();
2245
3604
  const ms = end - start;
@@ -2278,7 +3637,7 @@ function renderDecision(decision) {
2278
3637
  `;
2279
3638
  }
2280
3639
  function renderEvent(event) {
2281
- const time = formatDate3(new Date(event.ts).toISOString());
3640
+ const time = formatDate4(new Date(event.ts).toISOString());
2282
3641
  let content = "";
2283
3642
  let typeClass = "";
2284
3643
  const rawData = event.raw;
@@ -2324,7 +3683,7 @@ function renderEvent(event) {
2324
3683
  </div>
2325
3684
  `;
2326
3685
  }
2327
- function renderChapter(chapter, index) {
3686
+ function renderChapter2(chapter, index) {
2328
3687
  const events = chapter.events.map(renderEvent).join("");
2329
3688
  return `
2330
3689
  <div class="chapter">
@@ -2341,7 +3700,7 @@ function renderChapter(chapter, index) {
2341
3700
  </div>
2342
3701
  `;
2343
3702
  }
2344
- function renderRetrospective(trajectory) {
3703
+ function renderRetrospective2(trajectory) {
2345
3704
  if (!trajectory.retrospective) {
2346
3705
  return "";
2347
3706
  }
@@ -2373,7 +3732,7 @@ function renderRetrospective(trajectory) {
2373
3732
  }
2374
3733
  function generateTrajectoryHtml(trajectory) {
2375
3734
  const statusClass = getStatusClass(trajectory.status);
2376
- const duration = formatDuration(trajectory.startedAt, trajectory.completedAt);
3735
+ const duration = formatDuration2(trajectory.startedAt, trajectory.completedAt);
2377
3736
  const decisions = trajectory.chapters.flatMap(
2378
3737
  (ch) => ch.events.filter((e) => e.type === "decision" && e.raw).map((e) => e.raw).filter(
2379
3738
  (d) => d !== void 0 && typeof d.question === "string"
@@ -2392,7 +3751,7 @@ function generateTrajectoryHtml(trajectory) {
2392
3751
  Chapters (${trajectory.chapters.length})
2393
3752
  </h2>
2394
3753
  <div class="collapsible-content">
2395
- ${trajectory.chapters.map(renderChapter).join("")}
3754
+ ${trajectory.chapters.map(renderChapter2).join("")}
2396
3755
  </div>
2397
3756
  ` : "";
2398
3757
  const filesHtml = trajectory.filesChanged.length ? `
@@ -2433,7 +3792,7 @@ function generateTrajectoryHtml(trajectory) {
2433
3792
  </div>
2434
3793
  <div class="meta-item">
2435
3794
  <span class="meta-label">Started</span>
2436
- <span class="meta-value">${formatDate3(trajectory.startedAt)}</span>
3795
+ <span class="meta-value">${formatDate4(trajectory.startedAt)}</span>
2437
3796
  </div>
2438
3797
  ${trajectory.task.source ? `
2439
3798
  <div class="meta-item">
@@ -2448,7 +3807,7 @@ function generateTrajectoryHtml(trajectory) {
2448
3807
  </div>
2449
3808
  </div>
2450
3809
 
2451
- ${renderRetrospective(trajectory)}
3810
+ ${renderRetrospective2(trajectory)}
2452
3811
  ${decisionsHtml}
2453
3812
  ${chaptersHtml}
2454
3813
  ${filesHtml}
@@ -2512,9 +3871,9 @@ function registerExportCommand(program2) {
2512
3871
  openInBrowser(options.output);
2513
3872
  }
2514
3873
  } else if (options.open && options.format === "html") {
2515
- const outputDir = join5(process.cwd(), ".trajectories", "html");
3874
+ const outputDir = join7(process.cwd(), ".trajectories", "html");
2516
3875
  await mkdir4(outputDir, { recursive: true });
2517
- const filePath = join5(outputDir, `${trajectory.id}.html`);
3876
+ const filePath = join7(outputDir, `${trajectory.id}.html`);
2518
3877
  await writeFile4(filePath, output, "utf-8");
2519
3878
  console.log(`\u2713 Generated: ${filePath}`);
2520
3879
  openInBrowser(filePath);
@@ -2541,7 +3900,7 @@ function openInBrowser(path) {
2541
3900
  }
2542
3901
 
2543
3902
  // src/cli/commands/list.ts
2544
- import { existsSync as existsSync5 } from "fs";
3903
+ import { existsSync as existsSync6 } from "fs";
2545
3904
  function registerListCommand(program2) {
2546
3905
  program2.command("list").description("List and search trajectories").option(
2547
3906
  "-s, --status <status>",
@@ -2551,7 +3910,7 @@ function registerListCommand(program2) {
2551
3910
  let allTrajectories = [];
2552
3911
  const seenIds = /* @__PURE__ */ new Set();
2553
3912
  for (const searchPath of searchPaths) {
2554
- if (!existsSync5(searchPath)) {
3913
+ if (!existsSync6(searchPath)) {
2555
3914
  continue;
2556
3915
  }
2557
3916
  const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
@@ -2610,9 +3969,9 @@ function registerListCommand(program2) {
2610
3969
  const confidence = traj.confidence ? ` (${Math.round(traj.confidence * 100)}%)` : "";
2611
3970
  console.log(`${statusIcon} ${traj.id}`);
2612
3971
  console.log(` ${traj.title}${confidence}`);
2613
- console.log(` Started: ${formatDate4(traj.startedAt)}`);
3972
+ console.log(` Started: ${formatDate5(traj.startedAt)}`);
2614
3973
  if (traj.completedAt) {
2615
- console.log(` Completed: ${formatDate4(traj.completedAt)}`);
3974
+ console.log(` Completed: ${formatDate5(traj.completedAt)}`);
2616
3975
  }
2617
3976
  console.log("");
2618
3977
  }
@@ -2630,7 +3989,7 @@ function getStatusIcon(status) {
2630
3989
  return "\u2022";
2631
3990
  }
2632
3991
  }
2633
- function formatDate4(isoString) {
3992
+ function formatDate5(isoString) {
2634
3993
  return new Date(isoString).toLocaleDateString("en-US", {
2635
3994
  month: "short",
2636
3995
  day: "numeric",
@@ -2700,13 +4059,13 @@ function registerReflectCommand(program2) {
2700
4059
  }
2701
4060
 
2702
4061
  // src/cli/commands/show.ts
2703
- import { existsSync as existsSync6 } from "fs";
4062
+ import { existsSync as existsSync7 } from "fs";
2704
4063
  import { readFile as readFile2 } from "fs/promises";
2705
- import { join as join6 } from "path";
4064
+ import { join as join8 } from "path";
2706
4065
  async function findTrajectory(id) {
2707
4066
  const searchPaths = getSearchPaths();
2708
4067
  for (const searchPath of searchPaths) {
2709
- if (!existsSync6(searchPath)) {
4068
+ if (!existsSync7(searchPath)) {
2710
4069
  continue;
2711
4070
  }
2712
4071
  const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
@@ -2731,19 +4090,19 @@ async function findTrajectory(id) {
2731
4090
  async function findTraceFile(id) {
2732
4091
  const searchPaths = getSearchPaths();
2733
4092
  for (const searchPath of searchPaths) {
2734
- if (!existsSync6(searchPath)) {
4093
+ if (!existsSync7(searchPath)) {
2735
4094
  continue;
2736
4095
  }
2737
- const completedDir = join6(searchPath, "completed");
2738
- if (!existsSync6(completedDir)) {
4096
+ const completedDir = join8(searchPath, "completed");
4097
+ if (!existsSync7(completedDir)) {
2739
4098
  continue;
2740
4099
  }
2741
4100
  try {
2742
4101
  const { readdirSync } = await import("fs");
2743
4102
  const months = readdirSync(completedDir);
2744
4103
  for (const month of months) {
2745
- const tracePath = join6(completedDir, month, `${id}.trace.json`);
2746
- if (existsSync6(tracePath)) {
4104
+ const tracePath = join8(completedDir, month, `${id}.trace.json`);
4105
+ if (existsSync7(tracePath)) {
2747
4106
  let record;
2748
4107
  let migrated;
2749
4108
  try {
@@ -2894,7 +4253,10 @@ function registerStartCommand(program2) {
2894
4253
  program2.command("start <title>").description("Start a new trajectory").option("-t, --task <id>", "External task ID").option(
2895
4254
  "-s, --source <system>",
2896
4255
  "Task system (github, linear, jira, beads)"
2897
- ).option("--url <url>", "URL to external task").option("-a, --agent <name>", "Agent name (or set TRAJECTORIES_AGENT)").option("-p, --project <id>", "Project ID (or set TRAJECTORIES_PROJECT)").option("-q, --quiet", "Only output trajectory ID (for scripting)").action(async (title, options) => {
4256
+ ).option("--url <url>", "URL to external task").option("-a, --agent <name>", "Agent name (or set TRAJECTORIES_AGENT)").option("-p, --project <id>", "Project ID (or set TRAJECTORIES_PROJECT)").option(
4257
+ "-w, --workflow <id>",
4258
+ "Workflow run id (or set TRAJECTORIES_WORKFLOW_ID). Stamped onto the trajectory so `trail compact --workflow <id>` can collate a run."
4259
+ ).option("-q, --quiet", "Only output trajectory ID (for scripting)").action(async (title, options) => {
2898
4260
  const storage = new FileStorage();
2899
4261
  await storage.initialize();
2900
4262
  const active = await storage.getActive();
@@ -2917,12 +4279,16 @@ function registerStartCommand(program2) {
2917
4279
  }
2918
4280
  const agentName = options.agent ?? process.env.TRAJECTORIES_AGENT ?? void 0;
2919
4281
  const projectId = options.project ?? process.env.TRAJECTORIES_PROJECT ?? void 0;
4282
+ const workflowId = typeof options.workflow === "string" && options.workflow.trim() || typeof process.env.TRAJECTORIES_WORKFLOW_ID === "string" && process.env.TRAJECTORIES_WORKFLOW_ID.trim() || void 0;
2920
4283
  const startRef = captureGitState();
2921
4284
  let trajectory = createTrajectory({
2922
4285
  title,
2923
4286
  source,
2924
4287
  projectId
2925
4288
  });
4289
+ if (workflowId) {
4290
+ trajectory = { ...trajectory, workflowId };
4291
+ }
2926
4292
  if (startRef) {
2927
4293
  trajectory = {
2928
4294
  ...trajectory,
@@ -2965,7 +4331,7 @@ function registerStatusCommand(program2) {
2965
4331
  console.log('Start one with: trail start "Task description"');
2966
4332
  return;
2967
4333
  }
2968
- const duration = formatDuration2(
4334
+ const duration = formatDuration3(
2969
4335
  (/* @__PURE__ */ new Date()).getTime() - new Date(active.startedAt).getTime()
2970
4336
  );
2971
4337
  const eventCount = active.chapters.reduce(
@@ -3007,7 +4373,7 @@ Current Chapter: ${currentChapter.title}`);
3007
4373
  }
3008
4374
  });
3009
4375
  }
3010
- function formatDuration2(ms) {
4376
+ function formatDuration3(ms) {
3011
4377
  const seconds = Math.floor(ms / 1e3);
3012
4378
  const minutes = Math.floor(seconds / 60);
3013
4379
  const hours = Math.floor(minutes / 60);