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/{chunk-4MACDCF4.js → chunk-YI27BR5O.js} +176 -17
- package/dist/chunk-YI27BR5O.js.map +1 -0
- package/dist/cli/index.js +1524 -158
- package/dist/cli/index.js.map +1 -1
- package/dist/{index-Ce7r5yv0.d.ts → index-Bw0Jk4zO.d.ts} +46 -16
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.js +3 -1
- package/package.json +1 -1
- package/dist/chunk-4MACDCF4.js.map +0 -1
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.
|
|
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 {
|
|
884
|
-
import { existsSync as
|
|
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(
|
|
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("--
|
|
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
|
|
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
|
-
|
|
918
|
-
printCompactedSummary(compacted);
|
|
2096
|
+
printLLMDryRun(llmPlan, config.model, options.workflow);
|
|
919
2097
|
return;
|
|
920
2098
|
}
|
|
921
|
-
const
|
|
922
|
-
|
|
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 (!
|
|
2153
|
+
if (!existsSync3(searchPath)) continue;
|
|
947
2154
|
const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
|
|
948
2155
|
process.env.TRAJECTORIES_DATA_DIR = searchPath;
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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 =
|
|
1004
|
-
|
|
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 =
|
|
1028
|
-
if (!
|
|
2235
|
+
const indexPath = join4(searchPath, "index.json");
|
|
2236
|
+
if (!existsSync3(indexPath)) continue;
|
|
1029
2237
|
try {
|
|
1030
|
-
const indexContent =
|
|
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 =
|
|
1046
|
-
if (!
|
|
2253
|
+
const indexPath = join4(searchPath, "index.json");
|
|
2254
|
+
if (!existsSync3(indexPath)) continue;
|
|
1047
2255
|
try {
|
|
1048
|
-
const indexContent =
|
|
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
|
|
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 =
|
|
1220
|
-
if (!
|
|
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
|
|
2505
|
+
return join4(compactedDir, `${compacted.id}_${dateStr}.json`);
|
|
1225
2506
|
}
|
|
1226
|
-
function
|
|
1227
|
-
const dir =
|
|
1228
|
-
if (!
|
|
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: ${
|
|
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
|
-
|
|
1245
|
-
|
|
1246
|
-
console.log(
|
|
1247
|
-
|
|
1248
|
-
)
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
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 (
|
|
1254
|
-
console.log(
|
|
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
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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 >
|
|
1264
|
-
console.log(
|
|
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
|
|
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
|
|
2646
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1288
2647
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
1289
|
-
import { join as
|
|
2648
|
+
import { join as join5 } from "path";
|
|
1290
2649
|
|
|
1291
2650
|
// src/core/trace.ts
|
|
1292
|
-
import { execSync
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
1484
|
-
import { readFileSync as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 :
|
|
1611
|
-
const completedDir =
|
|
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 =
|
|
2972
|
+
const monthDir = join5(
|
|
1614
2973
|
completedDir,
|
|
1615
2974
|
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`
|
|
1616
2975
|
);
|
|
1617
|
-
if (!
|
|
2976
|
+
if (!existsSync4(monthDir)) {
|
|
1618
2977
|
await mkdir2(monthDir, { recursive: true });
|
|
1619
2978
|
}
|
|
1620
|
-
const tracePath =
|
|
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
|
|
1734
|
-
import { existsSync as
|
|
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
|
|
3095
|
+
import { join as join6 } from "path";
|
|
1737
3096
|
function getHooksDir() {
|
|
1738
3097
|
try {
|
|
1739
|
-
const gitDir =
|
|
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
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2231
3590
|
}
|
|
2232
|
-
function
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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">${
|
|
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
|
-
${
|
|
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 =
|
|
3874
|
+
const outputDir = join7(process.cwd(), ".trajectories", "html");
|
|
2516
3875
|
await mkdir4(outputDir, { recursive: true });
|
|
2517
|
-
const filePath =
|
|
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
|
|
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 (!
|
|
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: ${
|
|
3972
|
+
console.log(` Started: ${formatDate5(traj.startedAt)}`);
|
|
2614
3973
|
if (traj.completedAt) {
|
|
2615
|
-
console.log(` Completed: ${
|
|
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
|
|
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
|
|
4062
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2704
4063
|
import { readFile as readFile2 } from "fs/promises";
|
|
2705
|
-
import { join as
|
|
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 (!
|
|
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 (!
|
|
4093
|
+
if (!existsSync7(searchPath)) {
|
|
2735
4094
|
continue;
|
|
2736
4095
|
}
|
|
2737
|
-
const completedDir =
|
|
2738
|
-
if (!
|
|
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 =
|
|
2746
|
-
if (
|
|
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(
|
|
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 =
|
|
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
|
|
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);
|