agent-trajectories 0.5.2 → 0.5.4

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",
@@ -90,7 +92,7 @@ var DecisionSchema = z.object({
90
92
  });
91
93
  var AgentParticipationSchema = z.object({
92
94
  name: z.string().min(1, "Agent name is required"),
93
- role: z.enum(["lead", "contributor", "reviewer"]),
95
+ role: z.string().min(1, "Agent role is required"),
94
96
  joinedAt: z.string().datetime(),
95
97
  leftAt: z.string().datetime().optional()
96
98
  });
@@ -150,7 +152,7 @@ var TrajectoryTraceRefSchema = z.object({
150
152
  traceId: z.string().optional()
151
153
  });
152
154
  var TrajectorySchema = z.object({
153
- id: z.string().regex(/^traj_[a-z0-9]+$/, "Invalid trajectory ID format"),
155
+ id: z.string().regex(/^traj_[a-z0-9_]+$/, "Invalid trajectory ID format"),
154
156
  version: z.literal(1),
155
157
  task: TaskReferenceSchema,
156
158
  status: TrajectoryStatusSchema,
@@ -159,10 +161,11 @@ var TrajectorySchema = z.object({
159
161
  agents: z.array(AgentParticipationSchema),
160
162
  chapters: z.array(ChapterSchema),
161
163
  retrospective: RetrospectiveSchema.optional(),
162
- commits: z.array(z.string()),
163
- filesChanged: z.array(z.string()),
164
- projectId: z.string(),
165
- tags: z.array(z.string()),
164
+ commits: z.array(z.string()).default([]),
165
+ filesChanged: z.array(z.string()).default([]),
166
+ projectId: z.string().optional(),
167
+ workflowId: z.string().optional(),
168
+ tags: z.array(z.string()).default([]),
166
169
  _trace: TrajectoryTraceRefSchema.optional()
167
170
  });
168
171
  var CreateTrajectoryInputSchema = z.object({
@@ -564,11 +567,11 @@ function extractDecisions(trajectory) {
564
567
  }
565
568
 
566
569
  // src/storage/file.ts
567
- function expandPath(path) {
568
- if (path.startsWith("~")) {
569
- return join(process.env.HOME ?? "", path.slice(1));
570
+ function expandPath(path2) {
571
+ if (path2.startsWith("~")) {
572
+ return join(process.env.HOME ?? "", path2.slice(1));
570
573
  }
571
- return path;
574
+ return path2;
572
575
  }
573
576
  function getSearchPaths() {
574
577
  const searchPathsEnv = process.env.TRAJECTORIES_SEARCH_PATHS;
@@ -613,11 +616,129 @@ var FileStorage = class {
613
616
  trajectories: {}
614
617
  });
615
618
  }
619
+ await this.reconcileIndex();
620
+ }
621
+ /**
622
+ * Scan active/ and completed/ recursively and add any trajectory files
623
+ * missing from the index. Existing entries are preserved — reconcile
624
+ * only adds, never removes.
625
+ *
626
+ * Handles three on-disk layouts in completed/:
627
+ * - flat: completed/{id}.json (legacy workforce data)
628
+ * - monthly: completed/YYYY-MM/{id}.json (current save() writes)
629
+ * - nested: completed/.../{id}.json (defensive — any depth)
630
+ *
631
+ * Returns a ReconcileSummary so tests and CLI wrappers can observe
632
+ * outcomes without parsing logs. Only writes the index if anything was
633
+ * added.
634
+ */
635
+ async reconcileIndex() {
636
+ const summary = {
637
+ scanned: 0,
638
+ added: 0,
639
+ alreadyIndexed: 0,
640
+ skippedMalformedJson: 0,
641
+ skippedSchemaViolation: 0,
642
+ skippedIoError: 0
643
+ };
644
+ const index = await this.loadIndex();
645
+ const before = Object.keys(index.trajectories).length;
646
+ const discovered = [];
647
+ try {
648
+ const activeFiles = await readdir(this.activeDir);
649
+ for (const file of activeFiles) {
650
+ if (!file.endsWith(".json")) continue;
651
+ discovered.push(join(this.activeDir, file));
652
+ }
653
+ } catch (error) {
654
+ if (error.code !== "ENOENT") throw error;
655
+ }
656
+ await this.walkJsonFilesInto(this.completedDir, discovered);
657
+ for (const filePath of discovered) {
658
+ summary.scanned += 1;
659
+ const result = await this.readTrajectoryFile(filePath);
660
+ if (!result.ok) {
661
+ if (result.reason === "malformed_json") {
662
+ summary.skippedMalformedJson += 1;
663
+ } else if (result.reason === "schema_violation") {
664
+ summary.skippedSchemaViolation += 1;
665
+ } else {
666
+ summary.skippedIoError += 1;
667
+ }
668
+ continue;
669
+ }
670
+ const trajectory = result.trajectory;
671
+ if (index.trajectories[trajectory.id]) {
672
+ summary.alreadyIndexed += 1;
673
+ continue;
674
+ }
675
+ index.trajectories[trajectory.id] = {
676
+ title: trajectory.task.title,
677
+ status: trajectory.status,
678
+ startedAt: trajectory.startedAt,
679
+ completedAt: trajectory.completedAt,
680
+ path: filePath
681
+ };
682
+ summary.added += 1;
683
+ }
684
+ if (Object.keys(index.trajectories).length !== before) {
685
+ await this.saveIndex(index);
686
+ }
687
+ const hadSkips = summary.skippedMalformedJson + summary.skippedSchemaViolation + summary.skippedIoError > 0;
688
+ if (summary.added > 0 || hadSkips) {
689
+ const parts = [`reconciled ${summary.added}/${summary.scanned}`];
690
+ if (summary.skippedMalformedJson > 0) {
691
+ parts.push(`malformed: ${summary.skippedMalformedJson}`);
692
+ }
693
+ if (summary.skippedSchemaViolation > 0) {
694
+ parts.push(`invalid: ${summary.skippedSchemaViolation}`);
695
+ }
696
+ if (summary.skippedIoError > 0) {
697
+ parts.push(`io: ${summary.skippedIoError}`);
698
+ }
699
+ console.warn(`[trajectories] ${parts.join(", ")}`);
700
+ }
701
+ return summary;
616
702
  }
617
703
  /**
618
- * Save a trajectory
704
+ * Recursively collect all .json file paths under `dir` into `out`.
705
+ * Silently treats a missing directory as empty.
619
706
  */
620
- async save(trajectory) {
707
+ async walkJsonFilesInto(dir, out) {
708
+ let entries;
709
+ try {
710
+ entries = await readdir(dir, { withFileTypes: true });
711
+ } catch (error) {
712
+ if (error.code === "ENOENT") return;
713
+ throw error;
714
+ }
715
+ for (const entry of entries) {
716
+ const entryPath = join(dir, entry.name);
717
+ if (entry.isDirectory()) {
718
+ await this.walkJsonFilesInto(entryPath, out);
719
+ } else if (entry.isFile() && entry.name.endsWith(".json")) {
720
+ out.push(entryPath);
721
+ }
722
+ }
723
+ }
724
+ /**
725
+ * Save a trajectory.
726
+ *
727
+ * Validates the input against the trajectory schema before touching
728
+ * disk. Closes the historical read/write asymmetry where save() would
729
+ * happily write data that the reader then rejected, producing files
730
+ * that could never be loaded back.
731
+ */
732
+ async save(input) {
733
+ const validation = validateTrajectory(input);
734
+ if (!validation.success) {
735
+ const issues = validation.errors?.issues.map((issue) => {
736
+ const path2 = issue.path.length > 0 ? issue.path.join(".") : "root";
737
+ return `${path2}: ${issue.message}`;
738
+ }).join("; ") ?? "unknown validation error";
739
+ throw new Error(`Cannot save invalid trajectory: ${issues}`);
740
+ }
741
+ const trajectory = validation.data;
621
742
  const isCompleted = trajectory.status === "completed" || trajectory.status === "abandoned";
622
743
  let filePath;
623
744
  if (isCompleted) {
@@ -647,19 +768,23 @@ var FileStorage = class {
647
768
  async get(id) {
648
769
  const activePath = join(this.activeDir, `${id}.json`);
649
770
  if (existsSync(activePath)) {
650
- return this.readTrajectoryFile(activePath);
771
+ return this.readTrajectoryOrNull(activePath);
651
772
  }
652
773
  const index = await this.loadIndex();
653
774
  const entry = index.trajectories[id];
654
775
  if (entry?.path && existsSync(entry.path)) {
655
- return this.readTrajectoryFile(entry.path);
776
+ return this.readTrajectoryOrNull(entry.path);
656
777
  }
657
778
  try {
779
+ const flatPath = join(this.completedDir, `${id}.json`);
780
+ if (existsSync(flatPath)) {
781
+ return this.readTrajectoryOrNull(flatPath);
782
+ }
658
783
  const months = await readdir(this.completedDir);
659
784
  for (const month of months) {
660
785
  const filePath = join(this.completedDir, month, `${id}.json`);
661
786
  if (existsSync(filePath)) {
662
- return this.readTrajectoryFile(filePath);
787
+ return this.readTrajectoryOrNull(filePath);
663
788
  }
664
789
  }
665
790
  } catch (error) {
@@ -682,7 +807,7 @@ var FileStorage = class {
682
807
  let mostRecent = null;
683
808
  let mostRecentTime = 0;
684
809
  for (const file of jsonFiles) {
685
- const trajectory = await this.readTrajectoryFile(
810
+ const trajectory = await this.readTrajectoryOrNull(
686
811
  join(this.activeDir, file)
687
812
  );
688
813
  if (trajectory) {
@@ -810,20 +935,44 @@ var FileStorage = class {
810
935
  async close() {
811
936
  }
812
937
  // Private helpers
813
- async readTrajectoryFile(path) {
938
+ /**
939
+ * Read a trajectory file and return a tagged result so callers can
940
+ * distinguish missing files, malformed JSON, and schema violations.
941
+ *
942
+ * Does NOT log. Callers choose whether to warn, swallow, or throw.
943
+ */
944
+ async readTrajectoryFile(path2) {
945
+ let content;
814
946
  try {
815
- const content = await readFile(path, "utf-8");
816
- const data = JSON.parse(content);
817
- const validation = validateTrajectory(data);
818
- if (validation.success) {
819
- return validation.data;
820
- }
821
- console.error(`Invalid trajectory at ${path}:`, validation.errors);
822
- return null;
947
+ content = await readFile(path2, "utf-8");
823
948
  } catch (error) {
824
- console.error(`Failed to read trajectory at ${path}:`, error);
825
- return null;
949
+ return { ok: false, reason: "io_error", path: path2, error };
826
950
  }
951
+ let data;
952
+ try {
953
+ data = JSON.parse(content);
954
+ } catch (error) {
955
+ return { ok: false, reason: "malformed_json", path: path2, error };
956
+ }
957
+ const validation = validateTrajectory(data);
958
+ if (validation.success) {
959
+ return { ok: true, trajectory: validation.data };
960
+ }
961
+ return {
962
+ ok: false,
963
+ reason: "schema_violation",
964
+ path: path2,
965
+ error: validation.errors
966
+ };
967
+ }
968
+ /**
969
+ * Convenience wrapper for callers that only care whether they got a
970
+ * trajectory. Returns null for any failure and writes nothing to the
971
+ * console — so nothing leaks into test output or the CLI spinner.
972
+ */
973
+ async readTrajectoryOrNull(path2) {
974
+ const result = await this.readTrajectoryFile(path2);
975
+ return result.ok ? result.trajectory : null;
827
976
  }
828
977
  async loadIndex() {
829
978
  try {
@@ -880,9 +1029,1132 @@ function registerAbandonCommand(program2) {
880
1029
  }
881
1030
 
882
1031
  // src/cli/commands/compact.ts
883
- import { execSync } from "child_process";
884
- import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
1032
+ import { execFileSync } from "child_process";
1033
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
1034
+ import { dirname, join as join4 } from "path";
1035
+
1036
+ // src/compact/config.ts
1037
+ import { existsSync as existsSync2, readFileSync } from "fs";
885
1038
  import { join as join2 } from "path";
1039
+ var DEFAULT_CONFIG = {
1040
+ provider: "auto",
1041
+ model: void 0,
1042
+ maxInputTokens: 3e4,
1043
+ maxOutputTokens: 4e3,
1044
+ temperature: 0.3
1045
+ };
1046
+ function getCompactionConfig() {
1047
+ const fileConfig = loadFileConfig();
1048
+ return {
1049
+ provider: readStringEnv("TRAJECTORIES_LLM_PROVIDER") ?? readString(fileConfig.provider) ?? DEFAULT_CONFIG.provider,
1050
+ model: readStringEnv("TRAJECTORIES_LLM_MODEL") ?? readString(fileConfig.model) ?? DEFAULT_CONFIG.model,
1051
+ maxInputTokens: readNumberEnv("TRAJECTORIES_LLM_MAX_INPUT_TOKENS") ?? readNumber(fileConfig.maxInputTokens) ?? DEFAULT_CONFIG.maxInputTokens,
1052
+ maxOutputTokens: readNumberEnv("TRAJECTORIES_LLM_MAX_OUTPUT_TOKENS") ?? readNumber(fileConfig.maxOutputTokens) ?? DEFAULT_CONFIG.maxOutputTokens,
1053
+ temperature: readNumberEnv("TRAJECTORIES_LLM_TEMPERATURE") ?? readNumber(fileConfig.temperature) ?? DEFAULT_CONFIG.temperature
1054
+ };
1055
+ }
1056
+ function loadFileConfig() {
1057
+ const configPath = join2(getPrimaryConfigDir(), "config.json");
1058
+ if (!existsSync2(configPath)) {
1059
+ return {};
1060
+ }
1061
+ try {
1062
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
1063
+ if (!isRecord(raw)) {
1064
+ return {};
1065
+ }
1066
+ const merged = {};
1067
+ for (const section of [raw, raw.compaction, raw.llm]) {
1068
+ if (!isRecord(section)) {
1069
+ continue;
1070
+ }
1071
+ for (const [key, value] of Object.entries(section)) {
1072
+ if (key === "compaction" || key === "llm") {
1073
+ continue;
1074
+ }
1075
+ merged[key] = value;
1076
+ }
1077
+ }
1078
+ return {
1079
+ provider: readString(merged.provider),
1080
+ model: readString(merged.model),
1081
+ maxInputTokens: readNumber(merged.maxInputTokens),
1082
+ maxOutputTokens: readNumber(merged.maxOutputTokens),
1083
+ temperature: readNumber(merged.temperature)
1084
+ };
1085
+ } catch {
1086
+ return {};
1087
+ }
1088
+ }
1089
+ function getPrimaryConfigDir() {
1090
+ const searchPaths = getSearchPaths();
1091
+ return searchPaths[0] ?? join2(process.cwd(), ".trajectories");
1092
+ }
1093
+ function readStringEnv(name) {
1094
+ return readString(process.env[name]);
1095
+ }
1096
+ function readNumberEnv(name) {
1097
+ return readNumber(process.env[name]);
1098
+ }
1099
+ function readString(value) {
1100
+ if (typeof value !== "string") {
1101
+ return void 0;
1102
+ }
1103
+ const trimmed = value.trim();
1104
+ return trimmed.length > 0 ? trimmed : void 0;
1105
+ }
1106
+ function readNumber(value) {
1107
+ if (typeof value === "number" && Number.isFinite(value)) {
1108
+ return value;
1109
+ }
1110
+ if (typeof value !== "string") {
1111
+ return void 0;
1112
+ }
1113
+ const trimmed = value.trim();
1114
+ if (trimmed.length === 0) {
1115
+ return void 0;
1116
+ }
1117
+ const parsed = Number(trimmed);
1118
+ return Number.isFinite(parsed) ? parsed : void 0;
1119
+ }
1120
+ function isRecord(value) {
1121
+ return typeof value === "object" && value !== null;
1122
+ }
1123
+
1124
+ // src/compact/markdown.ts
1125
+ function generateCompactionMarkdown(compacted) {
1126
+ const dateRange = `${formatDate2(compacted.dateRange.start)} - ${formatDate2(compacted.dateRange.end)}`;
1127
+ const agents = compacted.summary.uniqueAgents.length > 0 ? compacted.summary.uniqueAgents.join(", ") : "None";
1128
+ const decisionRows = compacted.decisions.length > 0 ? compacted.decisions.map(
1129
+ (decision) => `| ${escapeTableCell(decision.question)} | ${escapeTableCell(decision.chosen)} | ${escapeTableCell(decision.impact)} |`
1130
+ ).join("\n") : "| None identified | | |";
1131
+ const conventions = compacted.conventions.length > 0 ? compacted.conventions.map(
1132
+ (convention) => `- **${convention.pattern || "Unnamed pattern"}**: ${convention.rationale || "No rationale captured."} (scope: ${convention.scope || "unspecified"})`
1133
+ ).join("\n") : "- None established.";
1134
+ const lessons = compacted.lessons.length > 0 ? compacted.lessons.map((lesson) => {
1135
+ const context = lesson.context ? ` (${lesson.context})` : "";
1136
+ const recommendation = lesson.recommendation ? ` - ${lesson.recommendation}` : "";
1137
+ return `- ${lesson.lesson}${context}${recommendation}`;
1138
+ }).join("\n") : "- None captured.";
1139
+ const openQuestions = compacted.openQuestions.length > 0 ? compacted.openQuestions.map((question) => `- ${question}`).join("\n") : "- None.";
1140
+ return [
1141
+ `# Trajectory Compaction: ${dateRange}`,
1142
+ "",
1143
+ "## Summary",
1144
+ compacted.narrative || "No narrative available.",
1145
+ "",
1146
+ `## Key Decisions (${compacted.decisions.length})`,
1147
+ "| Question | Decision | Impact |",
1148
+ "|----------|----------|--------|",
1149
+ decisionRows,
1150
+ "",
1151
+ "## Conventions Established",
1152
+ conventions,
1153
+ "",
1154
+ "## Lessons Learned",
1155
+ lessons,
1156
+ "",
1157
+ "## Open Questions",
1158
+ openQuestions,
1159
+ "",
1160
+ "## Stats",
1161
+ `- Sessions: ${compacted.sourceTrajectories.length}, Agents: ${agents}, Files: ${compacted.filesAffected.length}, Commits: ${compacted.commits.length}`,
1162
+ `- Date range: ${compacted.dateRange.start} - ${compacted.dateRange.end}`
1163
+ ].join("\n");
1164
+ }
1165
+ function formatDate2(value) {
1166
+ const date = new Date(value);
1167
+ if (Number.isNaN(date.getTime())) {
1168
+ return value;
1169
+ }
1170
+ return date.toISOString().slice(0, 10);
1171
+ }
1172
+ function escapeTableCell(value) {
1173
+ return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
1174
+ }
1175
+
1176
+ // src/compact/parser.ts
1177
+ function parseCompactionResponse(llmOutput) {
1178
+ const trimmed = llmOutput.trim();
1179
+ const parsedJson = parseJsonCandidate(trimmed) ?? parseJsonCandidate(extractFirstMarkdownJsonBlock(trimmed)) ?? parseJsonCandidate(extractBalancedJsonObject(trimmed));
1180
+ if (parsedJson) {
1181
+ return normalizeCompactionOutput(parsedJson, trimmed);
1182
+ }
1183
+ return normalizeCompactionOutput(extractFromProse(trimmed), trimmed);
1184
+ }
1185
+ function mergeCompactionWithMetadata(metadata, llmOutput) {
1186
+ return {
1187
+ ...metadata,
1188
+ ...llmOutput
1189
+ };
1190
+ }
1191
+ function parseJsonCandidate(candidate) {
1192
+ if (!candidate) {
1193
+ return null;
1194
+ }
1195
+ try {
1196
+ return JSON.parse(candidate);
1197
+ } catch {
1198
+ return null;
1199
+ }
1200
+ }
1201
+ function extractFirstMarkdownJsonBlock(text) {
1202
+ const match = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
1203
+ return match ? match[1].trim() : null;
1204
+ }
1205
+ function extractBalancedJsonObject(text) {
1206
+ const start = text.indexOf("{");
1207
+ if (start === -1) {
1208
+ return null;
1209
+ }
1210
+ let depth = 0;
1211
+ let inString = false;
1212
+ let escaped = false;
1213
+ for (let index = start; index < text.length; index += 1) {
1214
+ const char = text[index];
1215
+ if (escaped) {
1216
+ escaped = false;
1217
+ continue;
1218
+ }
1219
+ if (char === "\\") {
1220
+ escaped = true;
1221
+ continue;
1222
+ }
1223
+ if (char === '"') {
1224
+ inString = !inString;
1225
+ continue;
1226
+ }
1227
+ if (inString) {
1228
+ continue;
1229
+ }
1230
+ if (char === "{") {
1231
+ depth += 1;
1232
+ } else if (char === "}") {
1233
+ depth -= 1;
1234
+ if (depth === 0) {
1235
+ return text.slice(start, index + 1);
1236
+ }
1237
+ }
1238
+ }
1239
+ return null;
1240
+ }
1241
+ function extractFromProse(text) {
1242
+ const sections = splitSections(text);
1243
+ const narrativeSection = sections.narrative ?? sections.summary ?? leadingNarrative(text);
1244
+ return {
1245
+ narrative: normalizeText(narrativeSection),
1246
+ decisions: parseDecisionSection(
1247
+ sections["key decisions"] ?? sections.decisions ?? ""
1248
+ ),
1249
+ conventions: parseConventionSection(
1250
+ sections["conventions established"] ?? sections.conventions ?? ""
1251
+ ),
1252
+ lessons: parseLessonSection(
1253
+ sections["lessons learned"] ?? sections.lessons ?? ""
1254
+ ),
1255
+ openQuestions: parseStringList(
1256
+ sections["open questions"] ?? sections.questions ?? ""
1257
+ )
1258
+ };
1259
+ }
1260
+ function splitSections(text) {
1261
+ const matches = [...text.matchAll(/^##+\s+(.+?)\s*$/gm)];
1262
+ const sections = {};
1263
+ for (let index = 0; index < matches.length; index += 1) {
1264
+ const current = matches[index];
1265
+ const next = matches[index + 1];
1266
+ const title = normalizeHeading(current[1]);
1267
+ const start = current.index === void 0 ? 0 : current.index + current[0].length;
1268
+ const end = next?.index ?? text.length;
1269
+ sections[title] = text.slice(start, end).trim();
1270
+ }
1271
+ return sections;
1272
+ }
1273
+ function normalizeHeading(value) {
1274
+ return value.toLowerCase().replace(/\(\d+\)/g, "").replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
1275
+ }
1276
+ function leadingNarrative(text) {
1277
+ const beforeHeading = text.split(/^##+\s+/m, 1)[0] ?? "";
1278
+ const withoutCode = beforeHeading.replace(/```[\s\S]*?```/g, "").trim();
1279
+ return withoutCode;
1280
+ }
1281
+ function normalizeCompactionOutput(raw, fallbackNarrativeSource) {
1282
+ const candidate = isRecord2(raw) ? raw : {};
1283
+ const narrative = normalizeText(
1284
+ typeof candidate.narrative === "string" ? candidate.narrative : typeof candidate.summary === "string" ? candidate.summary : typeof candidate.overview === "string" ? candidate.overview : leadingNarrative(fallbackNarrativeSource)
1285
+ );
1286
+ return {
1287
+ narrative: narrative || normalizeText(fallbackNarrativeSource) || "No narrative provided.",
1288
+ decisions: normalizeDecisionArray(candidate.decisions),
1289
+ conventions: normalizeConventionArray(candidate.conventions),
1290
+ lessons: normalizeLessonArray(candidate.lessons),
1291
+ openQuestions: normalizeStringArray(
1292
+ candidate.openQuestions ?? candidate.open_questions ?? candidate.questions
1293
+ )
1294
+ };
1295
+ }
1296
+ function normalizeDecisionArray(value) {
1297
+ if (!Array.isArray(value)) {
1298
+ return [];
1299
+ }
1300
+ return value.map((entry) => {
1301
+ if (typeof entry === "string") {
1302
+ return {
1303
+ question: normalizeText(entry),
1304
+ chosen: "",
1305
+ reasoning: "",
1306
+ impact: ""
1307
+ };
1308
+ }
1309
+ if (!isRecord2(entry)) {
1310
+ return null;
1311
+ }
1312
+ return {
1313
+ question: readString2(entry, ["question", "prompt", "topic"]),
1314
+ chosen: readString2(entry, ["chosen", "decision", "answer"]),
1315
+ reasoning: readString2(entry, ["reasoning", "why", "rationale"]),
1316
+ impact: readString2(entry, ["impact", "result", "outcome"])
1317
+ };
1318
+ }).filter((entry) => {
1319
+ return entry !== null && hasContent(Object.values(entry));
1320
+ });
1321
+ }
1322
+ function normalizeConventionArray(value) {
1323
+ if (!Array.isArray(value)) {
1324
+ return [];
1325
+ }
1326
+ return value.map((entry) => {
1327
+ if (typeof entry === "string") {
1328
+ return {
1329
+ pattern: normalizeText(entry),
1330
+ rationale: "",
1331
+ scope: ""
1332
+ };
1333
+ }
1334
+ if (!isRecord2(entry)) {
1335
+ return null;
1336
+ }
1337
+ return {
1338
+ pattern: readString2(entry, ["pattern", "rule", "convention"]),
1339
+ rationale: readString2(entry, ["rationale", "reasoning", "why"]),
1340
+ scope: readString2(entry, ["scope", "appliesTo", "applies_to"])
1341
+ };
1342
+ }).filter((entry) => {
1343
+ return entry !== null && hasContent(Object.values(entry));
1344
+ });
1345
+ }
1346
+ function normalizeLessonArray(value) {
1347
+ if (!Array.isArray(value)) {
1348
+ return [];
1349
+ }
1350
+ return value.map((entry) => {
1351
+ if (typeof entry === "string") {
1352
+ return {
1353
+ lesson: normalizeText(entry),
1354
+ context: "",
1355
+ recommendation: ""
1356
+ };
1357
+ }
1358
+ if (!isRecord2(entry)) {
1359
+ return null;
1360
+ }
1361
+ return {
1362
+ lesson: readString2(entry, ["lesson", "learning", "takeaway"]),
1363
+ context: readString2(entry, ["context", "situation", "when"]),
1364
+ recommendation: readString2(entry, [
1365
+ "recommendation",
1366
+ "suggestion",
1367
+ "nextStep",
1368
+ "next_step"
1369
+ ])
1370
+ };
1371
+ }).filter((entry) => {
1372
+ return entry !== null && hasContent(Object.values(entry));
1373
+ });
1374
+ }
1375
+ function normalizeStringArray(value) {
1376
+ if (!Array.isArray(value)) {
1377
+ return [];
1378
+ }
1379
+ return value.map((entry) => typeof entry === "string" ? normalizeText(entry) : "").filter(Boolean);
1380
+ }
1381
+ function parseDecisionSection(section) {
1382
+ const tableDecisions = parseMarkdownTable(section).map((row) => ({
1383
+ question: row[0] ?? "",
1384
+ chosen: row[1] ?? "",
1385
+ reasoning: row[2] ?? "",
1386
+ impact: row[3] ?? row[2] ?? ""
1387
+ }));
1388
+ if (tableDecisions.length > 0) {
1389
+ return tableDecisions.filter((entry) => hasContent(Object.values(entry)));
1390
+ }
1391
+ return parseListItems(section).map((item) => {
1392
+ const fields = parseFieldMap(item);
1393
+ return {
1394
+ question: fields.question ?? fields.prompt ?? fields.topic ?? fields.title ?? item,
1395
+ chosen: fields.chosen ?? fields.decision ?? fields.answer ?? "",
1396
+ reasoning: fields.reasoning ?? fields.rationale ?? fields.why ?? "",
1397
+ impact: fields.impact ?? fields.outcome ?? fields.result ?? ""
1398
+ };
1399
+ }).filter((entry) => hasContent(Object.values(entry)));
1400
+ }
1401
+ function parseConventionSection(section) {
1402
+ return parseListItems(section).map((item) => {
1403
+ const emphasized = item.match(/^\*\*(.+?)\*\*:\s*(.+)$/);
1404
+ const scopeMatch = item.match(/\((?:scope|applies to):\s*([^)]+)\)\s*$/i);
1405
+ const withoutScope = scopeMatch ? item.slice(0, scopeMatch.index).trim() : item;
1406
+ if (emphasized) {
1407
+ return {
1408
+ pattern: normalizeText(emphasized[1]),
1409
+ rationale: normalizeText(
1410
+ withoutScope.replace(/^\*\*(.+?)\*\*:\s*/, "")
1411
+ ),
1412
+ scope: normalizeText(scopeMatch?.[1] ?? "")
1413
+ };
1414
+ }
1415
+ const fields = parseFieldMap(item);
1416
+ return {
1417
+ pattern: fields.pattern ?? fields.convention ?? fields.rule ?? item,
1418
+ rationale: fields.rationale ?? fields.reasoning ?? fields.why ?? "",
1419
+ scope: fields.scope ?? fields.applies ?? ""
1420
+ };
1421
+ }).filter((entry) => hasContent(Object.values(entry)));
1422
+ }
1423
+ function parseLessonSection(section) {
1424
+ return parseListItems(section).map((item) => {
1425
+ const fields = parseFieldMap(item);
1426
+ const dashParts = item.split(/\s[—-]\s/, 2);
1427
+ return {
1428
+ lesson: fields.lesson ?? fields.learning ?? fields.takeaway ?? dashParts[0] ?? item,
1429
+ context: fields.context ?? "",
1430
+ recommendation: fields.recommendation ?? fields.suggestion ?? fields.nextstep ?? dashParts[1] ?? ""
1431
+ };
1432
+ }).filter((entry) => hasContent(Object.values(entry)));
1433
+ }
1434
+ function parseStringList(section) {
1435
+ return parseListItems(section).map(normalizeText).filter(Boolean);
1436
+ }
1437
+ function parseMarkdownTable(section) {
1438
+ const lines = section.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("|"));
1439
+ if (lines.length < 2) {
1440
+ return [];
1441
+ }
1442
+ return lines.slice(1).filter((line) => !/^\|?\s*:?-{3,}/.test(line.replace(/\|/g, ""))).map(
1443
+ (line) => line.split("|").slice(1, -1).map((cell) => normalizeText(cell))
1444
+ );
1445
+ }
1446
+ function parseListItems(section) {
1447
+ return section.split("\n").map((line) => line.trim()).filter((line) => /^[-*] |\d+\.\s/.test(line)).map((line) => line.replace(/^[-*]\s+|\d+\.\s+/, "").trim()).filter(Boolean);
1448
+ }
1449
+ function parseFieldMap(item) {
1450
+ const normalized = item.replace(/\s+\|\s+/g, "; ");
1451
+ const segments = normalized.split(/;\s+/);
1452
+ const fields = {};
1453
+ for (const segment of segments) {
1454
+ const match = segment.match(/^([A-Za-z ]+):\s*(.+)$/);
1455
+ if (!match) {
1456
+ continue;
1457
+ }
1458
+ const key = match[1].toLowerCase().replace(/\s+/g, "");
1459
+ fields[key] = normalizeText(match[2]);
1460
+ }
1461
+ return fields;
1462
+ }
1463
+ function readString2(record, keys) {
1464
+ for (const key of keys) {
1465
+ const value = record[key];
1466
+ if (typeof value === "string") {
1467
+ return normalizeText(value);
1468
+ }
1469
+ }
1470
+ return "";
1471
+ }
1472
+ function isRecord2(value) {
1473
+ return typeof value === "object" && value !== null;
1474
+ }
1475
+ function normalizeText(value) {
1476
+ return value.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
1477
+ }
1478
+ function hasContent(values) {
1479
+ return values.some((value) => value.trim().length > 0);
1480
+ }
1481
+
1482
+ // src/compact/prompts.ts
1483
+ var COMPACTION_SYSTEM_PROMPT = `You are a technical analyst reviewing agent work sessions (trajectories).
1484
+ Your job is to produce a concise, insightful summary that captures:
1485
+ - What was accomplished and how
1486
+ - Key decisions and their reasoning
1487
+ - Patterns/conventions established that should be followed in future work
1488
+ - Lessons learned from challenges and failures
1489
+ - Open questions or unresolved issues
1490
+
1491
+ Be specific. Reference actual file paths, function names, and technical details.
1492
+ Don't be generic - this summary replaces the raw data.`;
1493
+ var COMPACTED_OUTPUT_SCHEMA = `{
1494
+ "narrative": "string",
1495
+ "decisions": [
1496
+ {
1497
+ "question": "string",
1498
+ "chosen": "string",
1499
+ "reasoning": "string",
1500
+ "impact": "string"
1501
+ }
1502
+ ],
1503
+ "conventions": [
1504
+ {
1505
+ "pattern": "string",
1506
+ "rationale": "string",
1507
+ "scope": "string"
1508
+ }
1509
+ ],
1510
+ "lessons": [
1511
+ {
1512
+ "lesson": "string",
1513
+ "context": "string",
1514
+ "recommendation": "string"
1515
+ }
1516
+ ],
1517
+ "openQuestions": ["string"]
1518
+ }`;
1519
+ function buildCompactionPrompt(serializedTrajectories, options = {}) {
1520
+ const focusAreas = options.focusAreas && options.focusAreas.length > 0 ? options.focusAreas.map((area) => `- ${area}`).join("\n") : [
1521
+ "- What work was attempted, completed, or abandoned",
1522
+ "- Why specific technical decisions were made",
1523
+ "- Which conventions should carry forward",
1524
+ "- What broke, what worked, and what should change next time"
1525
+ ].join("\n");
1526
+ 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.";
1527
+ const userPrompt = [
1528
+ "Review the following serialized agent trajectories and return a single JSON object.",
1529
+ "The JSON must match this schema exactly:",
1530
+ COMPACTED_OUTPUT_SCHEMA,
1531
+ "",
1532
+ "Requirements:",
1533
+ "- Output raw JSON only. Do not wrap it in markdown fences.",
1534
+ "- `narrative` should be 2-3 tight paragraphs.",
1535
+ "- `decisions`, `conventions`, and `lessons` must always be arrays, even if empty.",
1536
+ "- Prefer concrete file paths, symbols, commands, and implementation details over generic summaries.",
1537
+ maxOutputInstruction,
1538
+ "",
1539
+ "Focus areas:",
1540
+ focusAreas,
1541
+ "",
1542
+ "Serialized trajectories:",
1543
+ serializedTrajectories.trim()
1544
+ ].join("\n");
1545
+ return [
1546
+ {
1547
+ role: "system",
1548
+ content: COMPACTION_SYSTEM_PROMPT
1549
+ },
1550
+ {
1551
+ role: "user",
1552
+ content: userPrompt
1553
+ }
1554
+ ];
1555
+ }
1556
+
1557
+ // src/compact/provider.ts
1558
+ import { execFile, spawn } from "child_process";
1559
+ import { constants, accessSync } from "fs";
1560
+ import { homedir } from "os";
1561
+ import { join as join3 } from "path";
1562
+ import { promisify } from "util";
1563
+ var execFileAsync = promisify(execFile);
1564
+ var DEFAULT_OPENAI_MODEL = "gpt-4o";
1565
+ var DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514";
1566
+ var DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
1567
+ var DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com";
1568
+ var DEFAULT_MAX_TOKENS = 4096;
1569
+ var OpenAIProvider = class {
1570
+ apiKey;
1571
+ model;
1572
+ baseUrl;
1573
+ constructor(config = {}) {
1574
+ this.apiKey = config.apiKey?.trim() || process.env.OPENAI_API_KEY?.trim() || "";
1575
+ this.model = normalizeModel(config.model) ?? normalizeModel(process.env.TRAJECTORIES_LLM_MODEL) ?? DEFAULT_OPENAI_MODEL;
1576
+ this.baseUrl = config.baseUrl ?? process.env.OPENAI_BASE_URL ?? DEFAULT_OPENAI_BASE_URL;
1577
+ if (this.baseUrl !== DEFAULT_OPENAI_BASE_URL) {
1578
+ console.warn(
1579
+ `[trajectories] OpenAI base URL overridden to: ${this.baseUrl}`
1580
+ );
1581
+ }
1582
+ if (!this.apiKey) {
1583
+ throw new Error("OPENAI_API_KEY is required for OpenAIProvider");
1584
+ }
1585
+ }
1586
+ async complete(messages, options = {}) {
1587
+ const controller = new AbortController();
1588
+ const timeout = setTimeout(() => controller.abort(), 3e5);
1589
+ try {
1590
+ const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
1591
+ method: "POST",
1592
+ headers: {
1593
+ Authorization: `Bearer ${this.apiKey}`,
1594
+ "Content-Type": "application/json"
1595
+ },
1596
+ body: JSON.stringify({
1597
+ model: this.model,
1598
+ messages,
1599
+ max_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
1600
+ temperature: options.temperature ?? 0.2,
1601
+ response_format: options.jsonMode ? { type: "json_object" } : void 0
1602
+ }),
1603
+ signal: controller.signal
1604
+ });
1605
+ const body = await parseJson(response);
1606
+ if (!response.ok) {
1607
+ throw new Error(
1608
+ body.error?.message ?? `OpenAI request failed with status ${response.status}`
1609
+ );
1610
+ }
1611
+ const content = body.choices?.[0]?.message?.content;
1612
+ if (!content) {
1613
+ throw new Error("OpenAI response did not include completion content");
1614
+ }
1615
+ return content;
1616
+ } finally {
1617
+ clearTimeout(timeout);
1618
+ }
1619
+ }
1620
+ };
1621
+ var AnthropicProvider = class {
1622
+ apiKey;
1623
+ model;
1624
+ baseUrl;
1625
+ constructor(config = {}) {
1626
+ this.apiKey = config.apiKey?.trim() || process.env.ANTHROPIC_API_KEY?.trim() || "";
1627
+ this.model = normalizeModel(config.model) ?? normalizeModel(process.env.TRAJECTORIES_LLM_MODEL) ?? DEFAULT_ANTHROPIC_MODEL;
1628
+ this.baseUrl = config.baseUrl ?? process.env.ANTHROPIC_BASE_URL ?? DEFAULT_ANTHROPIC_BASE_URL;
1629
+ if (this.baseUrl !== DEFAULT_ANTHROPIC_BASE_URL) {
1630
+ console.warn(
1631
+ `[trajectories] Anthropic base URL overridden to: ${this.baseUrl}`
1632
+ );
1633
+ }
1634
+ if (!this.apiKey) {
1635
+ throw new Error("ANTHROPIC_API_KEY is required for AnthropicProvider");
1636
+ }
1637
+ }
1638
+ async complete(messages, options = {}) {
1639
+ const systemMessages = messages.filter((message) => message.role === "system").map((message) => message.content.trim()).filter(Boolean);
1640
+ const conversation = messages.filter((message) => message.role !== "system").map((message) => ({
1641
+ role: message.role,
1642
+ content: message.content
1643
+ }));
1644
+ if (conversation.length === 0) {
1645
+ throw new Error("AnthropicProvider requires at least one user message");
1646
+ }
1647
+ const controller = new AbortController();
1648
+ const timeout = setTimeout(() => controller.abort(), 3e5);
1649
+ try {
1650
+ const response = await fetch(`${this.baseUrl}/v1/messages`, {
1651
+ method: "POST",
1652
+ headers: {
1653
+ "Content-Type": "application/json",
1654
+ "anthropic-version": "2024-10-22",
1655
+ "x-api-key": this.apiKey
1656
+ },
1657
+ body: JSON.stringify({
1658
+ model: this.model,
1659
+ system: systemMessages.length > 0 ? systemMessages.join("\n\n") : void 0,
1660
+ messages: conversation,
1661
+ max_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
1662
+ temperature: options.temperature ?? 0.2
1663
+ }),
1664
+ signal: controller.signal
1665
+ });
1666
+ const body = await parseJson(response);
1667
+ if (!response.ok) {
1668
+ throw new Error(
1669
+ body.error?.message ?? `Anthropic request failed with status ${response.status}`
1670
+ );
1671
+ }
1672
+ const textBlocks = (body.content ?? []).filter(
1673
+ (block) => block.type === "text" && typeof block.text === "string"
1674
+ );
1675
+ const content = textBlocks.map((block) => block.text).join("\n").trim();
1676
+ if (!content) {
1677
+ throw new Error("Anthropic response did not include text content");
1678
+ }
1679
+ return content;
1680
+ } finally {
1681
+ clearTimeout(timeout);
1682
+ }
1683
+ }
1684
+ };
1685
+ var SUPPORTED_CLIS = ["claude", "codex", "gemini", "opencode"];
1686
+ var CLIProvider = class {
1687
+ cli;
1688
+ binaryPath;
1689
+ constructor(cli, binaryPath) {
1690
+ this.cli = cli;
1691
+ this.binaryPath = binaryPath;
1692
+ }
1693
+ get cliName() {
1694
+ return this.cli;
1695
+ }
1696
+ async complete(messages, _options = {}) {
1697
+ const prompt = messagesToPrompt(messages);
1698
+ const args = buildCliArgs(this.cli);
1699
+ const output = await spawnWithStdin(this.binaryPath, args, prompt);
1700
+ if (!output) {
1701
+ throw new Error(`${this.cli} CLI returned empty output`);
1702
+ }
1703
+ return output;
1704
+ }
1705
+ };
1706
+ function messagesToPrompt(messages) {
1707
+ const systemParts = [];
1708
+ const conversationParts = [];
1709
+ for (const msg of messages) {
1710
+ if (msg.role === "system") {
1711
+ systemParts.push(msg.content.trim());
1712
+ } else {
1713
+ conversationParts.push(msg.content.trim());
1714
+ }
1715
+ }
1716
+ const parts = [];
1717
+ if (systemParts.length > 0) {
1718
+ parts.push(systemParts.join("\n\n"));
1719
+ }
1720
+ if (conversationParts.length > 0) {
1721
+ parts.push(conversationParts.join("\n\n"));
1722
+ }
1723
+ return parts.join("\n\n---\n\n");
1724
+ }
1725
+ function buildCliArgs(cli) {
1726
+ switch (cli) {
1727
+ case "claude":
1728
+ return ["-p", "--output-format", "text"];
1729
+ case "codex":
1730
+ return ["exec", "-q"];
1731
+ case "gemini":
1732
+ return ["-p"];
1733
+ case "opencode":
1734
+ return ["run", "--no-color"];
1735
+ }
1736
+ }
1737
+ function spawnWithStdin(command, args, input) {
1738
+ return new Promise((resolve, reject) => {
1739
+ const child = spawn(command, args, {
1740
+ timeout: 3e5,
1741
+ stdio: ["pipe", "pipe", "pipe"]
1742
+ });
1743
+ const chunks = [];
1744
+ child.stdout.on("data", (chunk) => chunks.push(chunk));
1745
+ let stderr = "";
1746
+ child.stderr.on("data", (chunk) => {
1747
+ stderr += chunk.toString();
1748
+ });
1749
+ child.on("error", reject);
1750
+ child.on("close", (code) => {
1751
+ if (code !== 0) {
1752
+ reject(
1753
+ new Error(`CLI exited with code ${code}: ${stderr.slice(0, 200)}`)
1754
+ );
1755
+ } else {
1756
+ resolve(Buffer.concat(chunks).toString().trim());
1757
+ }
1758
+ });
1759
+ child.stdin.write(input);
1760
+ child.stdin.end();
1761
+ });
1762
+ }
1763
+ async function resolveProvider(config = {}) {
1764
+ const explicitProvider = (config.provider ?? process.env.TRAJECTORIES_LLM_PROVIDER)?.toLowerCase();
1765
+ const model = normalizeModel(config.model);
1766
+ if (explicitProvider === "openai") {
1767
+ return process.env.OPENAI_API_KEY ? new OpenAIProvider({ model }) : null;
1768
+ }
1769
+ if (explicitProvider === "anthropic") {
1770
+ return process.env.ANTHROPIC_API_KEY ? new AnthropicProvider({ model }) : null;
1771
+ }
1772
+ if (explicitProvider === "cli") {
1773
+ return resolveCLIProvider();
1774
+ }
1775
+ if (explicitProvider && explicitProvider !== "auto") {
1776
+ return null;
1777
+ }
1778
+ const cliProvider = await resolveCLIProvider();
1779
+ if (cliProvider) {
1780
+ return cliProvider;
1781
+ }
1782
+ if (process.env.OPENAI_API_KEY) {
1783
+ return new OpenAIProvider({ model });
1784
+ }
1785
+ if (process.env.ANTHROPIC_API_KEY) {
1786
+ return new AnthropicProvider({ model });
1787
+ }
1788
+ return null;
1789
+ }
1790
+ var CLI_SEARCH_PATHS = [
1791
+ "~/.local/bin",
1792
+ "~/.claude/local",
1793
+ "/usr/local/bin",
1794
+ "/opt/homebrew/bin"
1795
+ ];
1796
+ async function resolveCLIProvider() {
1797
+ const requestedCli = process.env.TRAJECTORIES_LLM_CLI?.trim().toLowerCase();
1798
+ const clisToTry = (() => {
1799
+ if (!requestedCli) {
1800
+ return SUPPORTED_CLIS;
1801
+ }
1802
+ if (SUPPORTED_CLIS.includes(requestedCli)) {
1803
+ return [requestedCli];
1804
+ }
1805
+ console.warn(
1806
+ `[trajectories] Unsupported TRAJECTORIES_LLM_CLI value "${requestedCli}", falling back to auto-detect`
1807
+ );
1808
+ return SUPPORTED_CLIS;
1809
+ })();
1810
+ for (const cli of clisToTry) {
1811
+ const path2 = await findBinary(cli);
1812
+ if (path2) {
1813
+ return new CLIProvider(cli, path2);
1814
+ }
1815
+ }
1816
+ return null;
1817
+ }
1818
+ async function findBinary(name) {
1819
+ try {
1820
+ const { stdout } = await execFileAsync("which", [name]);
1821
+ const path2 = stdout.trim();
1822
+ if (path2) return path2;
1823
+ } catch {
1824
+ }
1825
+ const home = homedir();
1826
+ for (const dir of CLI_SEARCH_PATHS) {
1827
+ const expanded = dir.startsWith("~/") ? join3(home, dir.slice(2)) : dir;
1828
+ const candidate = join3(expanded, name);
1829
+ try {
1830
+ accessSync(candidate, constants.X_OK);
1831
+ return candidate;
1832
+ } catch {
1833
+ }
1834
+ }
1835
+ return void 0;
1836
+ }
1837
+ function normalizeModel(value) {
1838
+ if (typeof value !== "string") {
1839
+ return void 0;
1840
+ }
1841
+ const trimmed = value.trim();
1842
+ return trimmed.length > 0 ? trimmed : void 0;
1843
+ }
1844
+ async function parseJson(response) {
1845
+ const text = await response.text();
1846
+ if (!text) {
1847
+ return {};
1848
+ }
1849
+ try {
1850
+ return JSON.parse(text);
1851
+ } catch {
1852
+ throw new Error(
1853
+ `Invalid JSON response (status ${response.status}, length ${text.length})`
1854
+ );
1855
+ }
1856
+ }
1857
+
1858
+ // src/compact/serializer.ts
1859
+ var DEFAULT_MAX_TOKENS2 = 3e4;
1860
+ var CHARS_PER_TOKEN = 4;
1861
+ var INCLUDED_SIGNIFICANCE = /* @__PURE__ */ new Set([
1862
+ "medium",
1863
+ "high",
1864
+ "critical"
1865
+ ]);
1866
+ function serializeForLLM(trajectories, maxTokens = DEFAULT_MAX_TOKENS2) {
1867
+ const maxChars = Math.max(0, maxTokens * CHARS_PER_TOKEN);
1868
+ const sessions = trajectories.map(renderSession);
1869
+ let document = joinSessions(sessions);
1870
+ if (document.length <= maxChars || sessions.length === 0) {
1871
+ return document;
1872
+ }
1873
+ const fixedChars = sessions.reduce(
1874
+ (total, session) => total + session.header.length + session.agents.length + session.decisions.length + session.findings.length + session.retrospective.length + session.filesAndCommits.length,
1875
+ 0
1876
+ );
1877
+ const chapterChars = sessions.reduce(
1878
+ (total, session) => total + session.chapters.reduce((sum, chapter) => sum + chapter.length, 0),
1879
+ 0
1880
+ );
1881
+ const remainingChapterChars = maxChars - fixedChars;
1882
+ if (remainingChapterChars <= 0 || chapterChars === 0) {
1883
+ return truncateText(document, maxChars);
1884
+ }
1885
+ const ratio = Math.min(1, remainingChapterChars / chapterChars);
1886
+ const truncatedSessions = sessions.map((session) => ({
1887
+ ...session,
1888
+ chapters: truncateChapters(
1889
+ session.chapters,
1890
+ session.chapters.reduce((sum, chapter) => sum + chapter.length, 0),
1891
+ ratio
1892
+ )
1893
+ }));
1894
+ document = joinSessions(truncatedSessions);
1895
+ return document.length <= maxChars ? document : truncateText(document, maxChars);
1896
+ }
1897
+ function renderSession(trajectory) {
1898
+ const sessionTitle = trajectory.task.title.trim() || trajectory.id;
1899
+ const duration = formatDuration(trajectory.startedAt, trajectory.completedAt);
1900
+ const header = [
1901
+ `## Session: ${sessionTitle} (${trajectory.status}, ${duration})`,
1902
+ trajectory.task.description ? `Description: ${trajectory.task.description}` : "",
1903
+ `Started: ${trajectory.startedAt}`,
1904
+ trajectory.completedAt ? `Completed: ${trajectory.completedAt}` : ""
1905
+ ].filter(Boolean).join("\n").concat("\n");
1906
+ const agents = trajectory.agents.length > 0 ? `Agents: ${trajectory.agents.map((agent) => `${agent.name} (${agent.role})`).join(", ")}
1907
+ ` : "Agents: none recorded\n";
1908
+ const chapters = trajectory.chapters.map(renderChapter);
1909
+ const decisions = renderDecisions(trajectory);
1910
+ const findings = renderFindings(trajectory);
1911
+ const retrospective = renderRetrospective(trajectory.retrospective);
1912
+ const filesAndCommits = [
1913
+ `Files changed: ${formatList(trajectory.filesChanged)}`,
1914
+ `Commits: ${formatList(trajectory.commits)}`
1915
+ ].join("\n").concat("\n");
1916
+ return {
1917
+ header,
1918
+ agents,
1919
+ chapters,
1920
+ decisions,
1921
+ findings,
1922
+ retrospective,
1923
+ filesAndCommits
1924
+ };
1925
+ }
1926
+ function renderChapter(chapter) {
1927
+ const lines = chapter.events.filter(shouldIncludeEvent).map((event) => formatEvent(event));
1928
+ const chapterBody = lines.length > 0 ? lines.map((line) => `- ${line}`).join("\n") : "- No medium/high/critical events captured";
1929
+ return [
1930
+ `### Chapter: ${chapter.title}`,
1931
+ `Agent: ${chapter.agentName}`,
1932
+ `Window: ${chapter.startedAt} -> ${chapter.endedAt ?? "ongoing"}`,
1933
+ chapterBody
1934
+ ].join("\n").concat("\n");
1935
+ }
1936
+ function renderDecisions(trajectory) {
1937
+ const seen = /* @__PURE__ */ new Set();
1938
+ const decisions = [];
1939
+ for (const chapter of trajectory.chapters) {
1940
+ for (const event of chapter.events) {
1941
+ if (event.type !== "decision") {
1942
+ continue;
1943
+ }
1944
+ const decision = asDecision(event.raw);
1945
+ if (!decision) {
1946
+ continue;
1947
+ }
1948
+ const key = `${decision.question}
1949
+ ${decision.chosen}
1950
+ ${decision.reasoning}`;
1951
+ if (!seen.has(key)) {
1952
+ seen.add(key);
1953
+ decisions.push(decision);
1954
+ }
1955
+ }
1956
+ }
1957
+ for (const decision of trajectory.retrospective?.decisions ?? []) {
1958
+ const key = `${decision.question}
1959
+ ${decision.chosen}
1960
+ ${decision.reasoning}`;
1961
+ if (!seen.has(key)) {
1962
+ seen.add(key);
1963
+ decisions.push(decision);
1964
+ }
1965
+ }
1966
+ if (decisions.length === 0) {
1967
+ return "Decisions:\n- None recorded\n";
1968
+ }
1969
+ return [
1970
+ "Decisions:",
1971
+ ...decisions.map(
1972
+ (decision) => [
1973
+ `- Question: ${decision.question}`,
1974
+ ` Chosen: ${decision.chosen}`,
1975
+ ` Reasoning: ${decision.reasoning}`
1976
+ ].join("\n")
1977
+ )
1978
+ ].join("\n").concat("\n");
1979
+ }
1980
+ function renderFindings(trajectory) {
1981
+ const findings = trajectory.chapters.flatMap(
1982
+ (chapter) => chapter.events.filter((event) => event.type === "finding").map((event) => asFinding(event.raw, event.content))
1983
+ );
1984
+ if (findings.length === 0) {
1985
+ return "Findings:\n- None recorded\n";
1986
+ }
1987
+ return [
1988
+ "Findings:",
1989
+ ...findings.map(
1990
+ (finding) => [
1991
+ `- What: ${finding.what}`,
1992
+ ` Where: ${finding.where}`,
1993
+ ` Significance: ${finding.significance}`
1994
+ ].join("\n")
1995
+ )
1996
+ ].join("\n").concat("\n");
1997
+ }
1998
+ function renderRetrospective(retrospective) {
1999
+ if (!retrospective) {
2000
+ return "Retrospective:\n- None recorded\n";
2001
+ }
2002
+ const lines = [
2003
+ "Retrospective:",
2004
+ `- Summary: ${retrospective.summary}`,
2005
+ ` Approach: ${retrospective.approach}`
2006
+ ];
2007
+ if (retrospective.challenges && retrospective.challenges.length > 0) {
2008
+ lines.push(` Challenges: ${retrospective.challenges.join("; ")}`);
2009
+ }
2010
+ if (retrospective.learnings && retrospective.learnings.length > 0) {
2011
+ lines.push(` Learnings: ${retrospective.learnings.join("; ")}`);
2012
+ }
2013
+ if (retrospective.suggestions && retrospective.suggestions.length > 0) {
2014
+ lines.push(` Suggestions: ${retrospective.suggestions.join("; ")}`);
2015
+ }
2016
+ if (retrospective.timeSpent) {
2017
+ lines.push(` Time spent: ${retrospective.timeSpent}`);
2018
+ }
2019
+ return lines.join("\n").concat("\n");
2020
+ }
2021
+ function shouldIncludeEvent(event) {
2022
+ if (event.type === "tool_call" || event.type === "tool_result") {
2023
+ return false;
2024
+ }
2025
+ return INCLUDED_SIGNIFICANCE.has(resolveSignificance(event));
2026
+ }
2027
+ function resolveSignificance(event) {
2028
+ if (event.significance) {
2029
+ return event.significance;
2030
+ }
2031
+ switch (event.type) {
2032
+ case "decision":
2033
+ case "finding":
2034
+ case "error":
2035
+ return "high";
2036
+ case "reflection":
2037
+ case "note":
2038
+ case "message_sent":
2039
+ case "message_received":
2040
+ return "medium";
2041
+ default:
2042
+ return "low";
2043
+ }
2044
+ }
2045
+ function formatEvent(event) {
2046
+ if (event.type === "decision") {
2047
+ const decision = asDecision(event.raw);
2048
+ if (decision) {
2049
+ return `[decision/${resolveSignificance(event)}] ${decision.question} -> ${decision.chosen}`;
2050
+ }
2051
+ }
2052
+ if (event.type === "finding") {
2053
+ const finding = asFinding(event.raw, event.content);
2054
+ return `[finding/${resolveSignificance(event)}] ${finding.what} @ ${finding.where}`;
2055
+ }
2056
+ return `[${event.type}/${resolveSignificance(event)}] ${event.content}`;
2057
+ }
2058
+ function asDecision(raw) {
2059
+ if (!raw || typeof raw !== "object") {
2060
+ return null;
2061
+ }
2062
+ const candidate = raw;
2063
+ if (typeof candidate.question !== "string" || typeof candidate.chosen !== "string" || typeof candidate.reasoning !== "string") {
2064
+ return null;
2065
+ }
2066
+ return {
2067
+ question: candidate.question,
2068
+ chosen: candidate.chosen,
2069
+ reasoning: candidate.reasoning,
2070
+ alternatives: Array.isArray(candidate.alternatives) ? candidate.alternatives : [],
2071
+ confidence: candidate.confidence
2072
+ };
2073
+ }
2074
+ function asFinding(raw, fallbackContent) {
2075
+ if (!raw || typeof raw !== "object") {
2076
+ return {
2077
+ what: fallbackContent,
2078
+ where: "unknown",
2079
+ significance: "Not structured",
2080
+ category: "other"
2081
+ };
2082
+ }
2083
+ const candidate = raw;
2084
+ return {
2085
+ what: typeof candidate.what === "string" && candidate.what.trim().length > 0 ? candidate.what : fallbackContent,
2086
+ where: typeof candidate.where === "string" && candidate.where.trim().length > 0 ? candidate.where : "unknown",
2087
+ significance: typeof candidate.significance === "string" && candidate.significance.trim().length > 0 ? candidate.significance : "Not structured",
2088
+ category: candidate.category ?? "other",
2089
+ suggestedAction: typeof candidate.suggestedAction === "string" ? candidate.suggestedAction : void 0,
2090
+ confidence: candidate.confidence
2091
+ };
2092
+ }
2093
+ function truncateChapters(chapters, totalChapterChars, ratio) {
2094
+ if (ratio >= 1 || totalChapterChars === 0) {
2095
+ return chapters;
2096
+ }
2097
+ let remaining = Math.floor(totalChapterChars * ratio);
2098
+ return chapters.map((chapter, index) => {
2099
+ if (remaining <= 0) {
2100
+ return "### Chapter: Truncated\n- Omitted due to token budget\n";
2101
+ }
2102
+ const proportionalTarget = index === chapters.length - 1 ? remaining : Math.floor(chapter.length * ratio);
2103
+ const allowance = Math.max(0, Math.min(chapter.length, proportionalTarget));
2104
+ remaining -= allowance;
2105
+ return truncateText(chapter, allowance);
2106
+ });
2107
+ }
2108
+ function joinSessions(sessions) {
2109
+ return sessions.map(
2110
+ (session) => [
2111
+ session.header,
2112
+ session.agents,
2113
+ ...session.chapters,
2114
+ session.decisions,
2115
+ session.findings,
2116
+ session.retrospective,
2117
+ session.filesAndCommits
2118
+ ].filter(Boolean).join("\n").trim()
2119
+ ).join("\n\n");
2120
+ }
2121
+ function truncateText(text, maxChars) {
2122
+ if (maxChars <= 0) {
2123
+ return "";
2124
+ }
2125
+ if (text.length <= maxChars) {
2126
+ return text;
2127
+ }
2128
+ if (maxChars <= 16) {
2129
+ return text.slice(0, maxChars);
2130
+ }
2131
+ return `${text.slice(0, maxChars - 16).trimEnd()}
2132
+ [truncated]
2133
+ `;
2134
+ }
2135
+ function formatList(values) {
2136
+ return values.length > 0 ? values.join(", ") : "none";
2137
+ }
2138
+ function formatDuration(startedAt, completedAt) {
2139
+ const start = new Date(startedAt).getTime();
2140
+ const end = new Date(completedAt ?? startedAt).getTime();
2141
+ const elapsedMs = Math.max(0, end - start);
2142
+ const minutes = Math.floor(elapsedMs / 6e4);
2143
+ const hours = Math.floor(minutes / 60);
2144
+ const remainingMinutes = minutes % 60;
2145
+ if (hours > 0 && remainingMinutes > 0) {
2146
+ return `${hours}h ${remainingMinutes}m`;
2147
+ }
2148
+ if (hours > 0) {
2149
+ return `${hours}h`;
2150
+ }
2151
+ if (minutes > 0) {
2152
+ return `${minutes}m`;
2153
+ }
2154
+ return completedAt ? "0m" : "ongoing";
2155
+ }
2156
+
2157
+ // src/cli/commands/compact.ts
886
2158
  function registerCompactCommand(program2) {
887
2159
  program2.command("compact").description(
888
2160
  "Compact trajectories into a summarized form (default: uncompacted only)"
@@ -892,16 +2164,22 @@ function registerCompactCommand(program2) {
892
2164
  ).option(
893
2165
  "--until <date>",
894
2166
  "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(
2167
+ ).option("--ids <ids>", "Comma-separated list of trajectory IDs to compact").option(
2168
+ "--workflow <id>",
2169
+ "Compact trajectories with the specified workflow ID"
2170
+ ).option("--pr <number>", "Compact trajectories associated with a PR number").option(
896
2171
  "--branch <name>",
897
2172
  "Compact trajectories with commits not in the specified branch (e.g., main)"
898
2173
  ).option(
899
2174
  "--commits <shas>",
900
2175
  "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) => {
2176
+ ).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(
2177
+ "--focus <areas>",
2178
+ "Comma-separated focus areas to emphasize in LLM compaction"
2179
+ ).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
2180
  const trajectories = await loadTrajectories(options);
903
2181
  if (trajectories.length === 0) {
904
- if (options.all || options.since || options.ids || options.pr || options.branch || options.commits) {
2182
+ if (options.all || options.since || options.ids || options.workflow || options.pr || options.branch || options.commits) {
905
2183
  console.log("No trajectories found matching criteria");
906
2184
  } else {
907
2185
  console.log(
@@ -912,17 +2190,92 @@ function registerCompactCommand(program2) {
912
2190
  }
913
2191
  console.log(`Compacting ${trajectories.length} trajectories...
914
2192
  `);
915
- const compacted = compactTrajectories(trajectories);
2193
+ const config = getCompactionConfig();
2194
+ const provider = await resolveProvider(config);
2195
+ const useLLM = shouldUseLLM(options, provider !== null);
2196
+ const markdownEnabled = options.markdown !== false;
2197
+ const mechanicalCompacted = compactTrajectories(
2198
+ trajectories,
2199
+ options.workflow
2200
+ );
2201
+ if (!useLLM || provider === null) {
2202
+ if (options.llm && provider === null && !options.mechanical) {
2203
+ console.log(
2204
+ "No LLM provider detected; falling back to mechanical compaction.\n"
2205
+ );
2206
+ }
2207
+ if (options.dryRun) {
2208
+ console.log("=== DRY RUN - Preview ===\n");
2209
+ printCompactedSummary(mechanicalCompacted);
2210
+ return;
2211
+ }
2212
+ const outputPath2 = options.output || getDefaultOutputPath(mechanicalCompacted, options.workflow);
2213
+ saveCompactionArtifacts(
2214
+ mechanicalCompacted,
2215
+ outputPath2,
2216
+ markdownEnabled
2217
+ );
2218
+ await markTrajectoriesAsCompacted(trajectories, mechanicalCompacted.id);
2219
+ console.log(`
2220
+ Compacted trajectory saved to: ${outputPath2}`);
2221
+ if (markdownEnabled) {
2222
+ console.log(
2223
+ `Markdown summary saved to: ${getMarkdownOutputPath(outputPath2)}`
2224
+ );
2225
+ }
2226
+ printCompactedSummary(mechanicalCompacted);
2227
+ return;
2228
+ }
2229
+ const llmPlan = buildLLMCompactionPlan(
2230
+ trajectories,
2231
+ parseFocusAreas(options.focus),
2232
+ config.maxInputTokens,
2233
+ config.maxOutputTokens
2234
+ );
2235
+ console.log(
2236
+ `Using ${getProviderLabel(provider)} compaction${config.model ? ` with model ${config.model}` : ""}.`
2237
+ );
2238
+ console.log(
2239
+ `Estimated: ~${llmPlan.estimatedInputTokens} input tokens, ~${llmPlan.estimatedOutputTokens} output tokens`
2240
+ );
916
2241
  if (options.dryRun) {
917
- console.log("=== DRY RUN - Preview ===\n");
918
- printCompactedSummary(compacted);
2242
+ printLLMDryRun(llmPlan, config.model, options.workflow);
919
2243
  return;
920
2244
  }
921
- const outputPath = options.output || getDefaultOutputPath(compacted);
922
- saveCompactedTrajectory(compacted, outputPath);
2245
+ const llmOutput = await provider.complete(llmPlan.messages, {
2246
+ maxTokens: config.maxOutputTokens,
2247
+ temperature: config.temperature,
2248
+ jsonMode: provider instanceof OpenAIProvider
2249
+ });
2250
+ const llmCompacted = parseCompactionResponse(llmOutput);
2251
+ const mergedCompaction = mergeCompactionWithMetadata(
2252
+ {
2253
+ id: mechanicalCompacted.id,
2254
+ version: mechanicalCompacted.version,
2255
+ type: mechanicalCompacted.type,
2256
+ compactedAt: mechanicalCompacted.compactedAt,
2257
+ sourceTrajectories: mechanicalCompacted.sourceTrajectories,
2258
+ dateRange: mechanicalCompacted.dateRange,
2259
+ summary: mechanicalCompacted.summary,
2260
+ filesAffected: mechanicalCompacted.filesAffected,
2261
+ commits: mechanicalCompacted.commits
2262
+ },
2263
+ llmCompacted
2264
+ );
2265
+ const compacted = {
2266
+ ...mechanicalCompacted,
2267
+ ...mergedCompaction
2268
+ };
2269
+ const outputPath = options.output || getDefaultOutputPath(compacted, options.workflow);
2270
+ saveCompactionArtifacts(compacted, outputPath, markdownEnabled);
923
2271
  await markTrajectoriesAsCompacted(trajectories, compacted.id);
924
2272
  console.log(`
925
2273
  Compacted trajectory saved to: ${outputPath}`);
2274
+ if (markdownEnabled) {
2275
+ console.log(
2276
+ `Markdown summary saved to: ${getMarkdownOutputPath(outputPath)}`
2277
+ );
2278
+ }
926
2279
  printCompactedSummary(compacted);
927
2280
  });
928
2281
  }
@@ -943,55 +2296,55 @@ async function loadTrajectories(options) {
943
2296
  const searchPaths = getSearchPaths();
944
2297
  const seenIds = /* @__PURE__ */ new Set();
945
2298
  for (const searchPath of searchPaths) {
946
- if (!existsSync2(searchPath)) continue;
2299
+ if (!existsSync3(searchPath)) continue;
947
2300
  const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
948
2301
  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);
2302
+ const storage = new FileStorage();
2303
+ if (originalDataDir !== void 0) {
2304
+ process.env.TRAJECTORIES_DATA_DIR = originalDataDir;
2305
+ } else {
2306
+ delete process.env.TRAJECTORIES_DATA_DIR;
2307
+ }
2308
+ await storage.initialize();
2309
+ const summaries = await storage.list({
2310
+ status: "completed",
2311
+ limit: Number.MAX_SAFE_INTEGER
2312
+ });
2313
+ for (const summary of summaries) {
2314
+ if (seenIds.has(summary.id)) continue;
2315
+ if (compactedIds.has(summary.id)) continue;
2316
+ if (targetIds && !targetIds.includes(summary.id)) continue;
2317
+ const startDate = new Date(summary.startedAt);
2318
+ if (sinceDate && startDate < sinceDate) continue;
2319
+ if (untilDate && startDate > untilDate) continue;
2320
+ const trajectory = await storage.get(summary.id);
2321
+ if (trajectory) {
2322
+ seenIds.add(summary.id);
2323
+ if (options.workflow && trajectory.workflowId !== options.workflow) {
2324
+ continue;
988
2325
  }
989
- }
990
- } finally {
991
- if (originalDataDir !== void 0) {
992
- process.env.TRAJECTORIES_DATA_DIR = originalDataDir;
993
- } else {
994
- delete process.env.TRAJECTORIES_DATA_DIR;
2326
+ if (options.pr) {
2327
+ const escaped = options.pr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2328
+ const prPattern = new RegExp(
2329
+ `#${escaped}\\b|\\bPR\\s*#?\\s*${escaped}\\b`,
2330
+ "i"
2331
+ );
2332
+ const matchesPR = prPattern.test(trajectory.task.title) || prPattern.test(trajectory.task.description || "") || trajectory.commits.some((c) => prPattern.test(c));
2333
+ if (!matchesPR) continue;
2334
+ }
2335
+ if (branchCommits) {
2336
+ const hasMatchingCommit = trajectory.commits.some(
2337
+ (c) => branchCommits.has(c.slice(0, 7)) || branchCommits.has(c)
2338
+ );
2339
+ if (!hasMatchingCommit && trajectory.commits.length > 0) continue;
2340
+ }
2341
+ if (targetCommits) {
2342
+ const hasMatchingCommit = trajectory.commits.some(
2343
+ (c) => targetCommits.has(c) || targetCommits.has(c.slice(0, 7))
2344
+ );
2345
+ if (!hasMatchingCommit) continue;
2346
+ }
2347
+ trajectories.push(trajectory);
995
2348
  }
996
2349
  }
997
2350
  }
@@ -1000,8 +2353,9 @@ async function loadTrajectories(options) {
1000
2353
  function getBranchCommits(targetBranch) {
1001
2354
  const commits = /* @__PURE__ */ new Set();
1002
2355
  try {
1003
- const output = execSync(
1004
- `git log '${targetBranch.replace(/'/g, "'\\''")}'..HEAD --format=%H`,
2356
+ const output = execFileSync(
2357
+ "git",
2358
+ ["log", `${targetBranch}..HEAD`, "--format=%H"],
1005
2359
  {
1006
2360
  encoding: "utf-8",
1007
2361
  stdio: ["pipe", "pipe", "pipe"]
@@ -1024,10 +2378,10 @@ function getCompactedTrajectoryIds() {
1024
2378
  const compacted = /* @__PURE__ */ new Set();
1025
2379
  const searchPaths = getSearchPaths();
1026
2380
  for (const searchPath of searchPaths) {
1027
- const indexPath = join2(searchPath, "index.json");
1028
- if (!existsSync2(indexPath)) continue;
2381
+ const indexPath = join4(searchPath, "index.json");
2382
+ if (!existsSync3(indexPath)) continue;
1029
2383
  try {
1030
- const indexContent = readFileSync(indexPath, "utf-8");
2384
+ const indexContent = readFileSync2(indexPath, "utf-8");
1031
2385
  const index = JSON.parse(indexContent);
1032
2386
  for (const [id, entry] of Object.entries(index.trajectories || {})) {
1033
2387
  if (entry.compactedInto) {
@@ -1042,10 +2396,10 @@ function getCompactedTrajectoryIds() {
1042
2396
  async function markTrajectoriesAsCompacted(trajectories, compactedIntoId) {
1043
2397
  const searchPaths = getSearchPaths();
1044
2398
  for (const searchPath of searchPaths) {
1045
- const indexPath = join2(searchPath, "index.json");
1046
- if (!existsSync2(indexPath)) continue;
2399
+ const indexPath = join4(searchPath, "index.json");
2400
+ if (!existsSync3(indexPath)) continue;
1047
2401
  try {
1048
- const indexContent = readFileSync(indexPath, "utf-8");
2402
+ const indexContent = readFileSync2(indexPath, "utf-8");
1049
2403
  const index = JSON.parse(indexContent);
1050
2404
  let updated = false;
1051
2405
  for (const traj of trajectories) {
@@ -1081,7 +2435,7 @@ function parseRelativeDate(input) {
1081
2435
  }
1082
2436
  return new Date(input);
1083
2437
  }
1084
- function compactTrajectories(trajectories) {
2438
+ function compactTrajectories(trajectories, workflowId) {
1085
2439
  const allDecisions = [];
1086
2440
  const allLearnings = [];
1087
2441
  const allFindings = [];
@@ -1144,6 +2498,7 @@ function compactTrajectories(trajectories) {
1144
2498
  version: 1,
1145
2499
  type: "compacted",
1146
2500
  compactedAt: (/* @__PURE__ */ new Date()).toISOString(),
2501
+ workflowId,
1147
2502
  sourceTrajectories: trajectories.map((t) => t.id),
1148
2503
  dateRange: {
1149
2504
  start: minDate.toISOString(),
@@ -1214,56 +2569,206 @@ function groupDecisions(decisions) {
1214
2569
  (a, b) => b.decisions.length - a.decisions.length
1215
2570
  );
1216
2571
  }
1217
- function getDefaultOutputPath(compacted) {
2572
+ function shouldUseLLM(options, providerAvailable) {
2573
+ if (options.mechanical) {
2574
+ return false;
2575
+ }
2576
+ if (options.llm === false) {
2577
+ return false;
2578
+ }
2579
+ if (options.llm === true) {
2580
+ return providerAvailable;
2581
+ }
2582
+ return providerAvailable;
2583
+ }
2584
+ function buildLLMCompactionPlan(trajectories, focusAreas, maxInputTokens, maxOutputTokens) {
2585
+ const serialized = serializeForLLM(trajectories, maxInputTokens);
2586
+ const messages = buildCompactionPrompt(serialized, {
2587
+ focusAreas,
2588
+ maxOutputTokens
2589
+ });
2590
+ return {
2591
+ messages,
2592
+ estimatedInputTokens: estimateTokens(
2593
+ messages.map((message) => message.content).join("\n\n")
2594
+ ),
2595
+ estimatedOutputTokens: maxOutputTokens,
2596
+ focusAreas
2597
+ };
2598
+ }
2599
+ function parseFocusAreas(focus) {
2600
+ if (!focus) {
2601
+ return [];
2602
+ }
2603
+ return focus.split(",").map((area) => area.trim()).filter(Boolean);
2604
+ }
2605
+ function estimateTokens(text) {
2606
+ return Math.max(1, Math.ceil(text.length / 4));
2607
+ }
2608
+ function printLLMDryRun(plan, model, workflowId) {
2609
+ console.log("=== DRY RUN - LLM Prompt Preview ===\n");
2610
+ console.log(
2611
+ `Estimated: ~${plan.estimatedInputTokens} input tokens, ~${plan.estimatedOutputTokens} output tokens`
2612
+ );
2613
+ if (model) {
2614
+ console.log(`Configured model: ${model}`);
2615
+ }
2616
+ if (workflowId) {
2617
+ console.log(`Workflow: ${workflowId}`);
2618
+ }
2619
+ if (plan.focusAreas.length > 0) {
2620
+ console.log(`Focus: ${plan.focusAreas.join(", ")}`);
2621
+ }
2622
+ console.log("");
2623
+ for (const message of plan.messages) {
2624
+ console.log(`[${message.role.toUpperCase()}]`);
2625
+ console.log(message.content);
2626
+ console.log("");
2627
+ }
2628
+ }
2629
+ function getProviderLabel(provider) {
2630
+ if (provider instanceof OpenAIProvider) {
2631
+ return "OpenAI";
2632
+ }
2633
+ if (provider instanceof AnthropicProvider) {
2634
+ return "Anthropic";
2635
+ }
2636
+ if (provider instanceof CLIProvider) {
2637
+ return `CLI (${provider.cliName})`;
2638
+ }
2639
+ return "LLM";
2640
+ }
2641
+ function getDefaultOutputPath(compacted, workflowId) {
1218
2642
  const trajDir = process.env.TRAJECTORIES_DATA_DIR || ".trajectories";
1219
- const compactedDir = join2(trajDir, "compacted");
1220
- if (!existsSync2(compactedDir)) {
2643
+ const compactedDir = join4(trajDir, "compacted");
2644
+ if (!existsSync3(compactedDir)) {
1221
2645
  mkdirSync(compactedDir, { recursive: true });
1222
2646
  }
2647
+ if (workflowId) {
2648
+ return join4(compactedDir, `workflow-${workflowId}.json`);
2649
+ }
1223
2650
  const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1224
- return join2(compactedDir, `${compacted.id}_${dateStr}.json`);
2651
+ return join4(compactedDir, `${compacted.id}_${dateStr}.json`);
1225
2652
  }
1226
- function saveCompactedTrajectory(compacted, outputPath) {
1227
- const dir = join2(outputPath, "..");
1228
- if (!existsSync2(dir)) {
2653
+ function saveCompactionArtifacts(compacted, outputPath, markdownEnabled) {
2654
+ const dir = dirname(outputPath);
2655
+ if (!existsSync3(dir)) {
1229
2656
  mkdirSync(dir, { recursive: true });
1230
2657
  }
1231
2658
  writeFileSync(outputPath, JSON.stringify(compacted, null, 2));
2659
+ if (markdownEnabled) {
2660
+ writeFileSync(
2661
+ getMarkdownOutputPath(outputPath),
2662
+ renderCompactionMarkdown(compacted)
2663
+ );
2664
+ }
2665
+ }
2666
+ function getMarkdownOutputPath(outputPath) {
2667
+ return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".md") : `${outputPath}.md`;
2668
+ }
2669
+ function renderCompactionMarkdown(compacted) {
2670
+ if (compacted.narrative) {
2671
+ return generateCompactionMarkdown(
2672
+ compacted
2673
+ );
2674
+ }
2675
+ const decisionGroups = compacted.decisionGroups.length > 0 ? compacted.decisionGroups.map((group) => {
2676
+ const decisions = group.decisions.length > 0 ? group.decisions.map(
2677
+ (decision) => `- ${decision.question} -> ${decision.chosen} (${decision.fromTrajectory})`
2678
+ ).join("\n") : "- None";
2679
+ return `## ${capitalize(group.category)}
2680
+ ${decisions}`;
2681
+ }).join("\n\n") : "## Decision Groups\n- None";
2682
+ const learnings = compacted.keyLearnings.length > 0 ? compacted.keyLearnings.map((learning) => `- ${learning}`).join("\n") : "- None";
2683
+ const findings = compacted.keyFindings.length > 0 ? compacted.keyFindings.map((finding) => `- ${finding}`).join("\n") : "- None";
2684
+ return [
2685
+ `# Trajectory Compaction: ${formatDate3(compacted.dateRange.start)} - ${formatDate3(compacted.dateRange.end)}`,
2686
+ "",
2687
+ "## Summary",
2688
+ `- Sessions: ${compacted.sourceTrajectories.length}`,
2689
+ ...compacted.workflowId ? [`- Workflow: ${compacted.workflowId}`] : [],
2690
+ `- Decisions: ${compacted.summary.totalDecisions}`,
2691
+ `- Events: ${compacted.summary.totalEvents}`,
2692
+ `- Agents: ${compacted.summary.uniqueAgents.join(", ") || "None"}`,
2693
+ `- Files: ${compacted.filesAffected.length}`,
2694
+ `- Commits: ${compacted.commits.length}`,
2695
+ "",
2696
+ decisionGroups,
2697
+ "",
2698
+ "## Key Learnings",
2699
+ learnings,
2700
+ "",
2701
+ "## Key Findings",
2702
+ findings
2703
+ ].join("\n");
1232
2704
  }
1233
2705
  function printCompactedSummary(compacted) {
1234
2706
  console.log("=== Compacted Trajectory Summary ===\n");
1235
2707
  console.log(`ID: ${compacted.id}`);
2708
+ if (compacted.workflowId) {
2709
+ console.log(`Workflow: ${compacted.workflowId}`);
2710
+ }
1236
2711
  console.log(`Source trajectories: ${compacted.sourceTrajectories.length}`);
1237
2712
  console.log(
1238
- `Date range: ${formatDate2(compacted.dateRange.start)} - ${formatDate2(compacted.dateRange.end)}`
2713
+ `Date range: ${formatDate3(compacted.dateRange.start)} - ${formatDate3(compacted.dateRange.end)}`
1239
2714
  );
1240
2715
  console.log(`Total decisions: ${compacted.summary.totalDecisions}`);
1241
2716
  console.log(`Total events: ${compacted.summary.totalEvents}`);
1242
2717
  console.log(`Agents: ${compacted.summary.uniqueAgents.join(", ")}`);
1243
2718
  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}`);
2719
+ if (compacted.narrative) {
2720
+ console.log("=== Narrative ===\n");
2721
+ console.log(compacted.narrative);
2722
+ console.log("");
2723
+ if (compacted.decisions && compacted.decisions.length > 0) {
2724
+ console.log("=== Key Decisions ===\n");
2725
+ for (const decision of compacted.decisions.slice(0, 5)) {
2726
+ console.log(` - ${decision.question}`);
2727
+ console.log(` Chosen: ${decision.chosen}`);
2728
+ if (decision.impact) {
2729
+ console.log(` Impact: ${decision.impact}`);
2730
+ }
2731
+ }
2732
+ if (compacted.decisions.length > 5) {
2733
+ console.log(` ... and ${compacted.decisions.length - 5} more`);
2734
+ }
2735
+ console.log("");
1252
2736
  }
1253
- if (group.decisions.length > 3) {
1254
- console.log(` ... and ${group.decisions.length - 3} more`);
2737
+ if (compacted.openQuestions && compacted.openQuestions.length > 0) {
2738
+ console.log("=== Open Questions ===\n");
2739
+ for (const question of compacted.openQuestions.slice(0, 5)) {
2740
+ console.log(` - ${question}`);
2741
+ }
2742
+ if (compacted.openQuestions.length > 5) {
2743
+ console.log(` ... and ${compacted.openQuestions.length - 5} more`);
2744
+ }
2745
+ console.log("");
1255
2746
  }
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}`);
2747
+ } else {
2748
+ console.log("=== Decision Groups ===\n");
2749
+ for (const group of compacted.decisionGroups) {
2750
+ console.log(
2751
+ `${capitalize(group.category)} (${group.decisions.length} decisions):`
2752
+ );
2753
+ for (const decision of group.decisions.slice(0, 3)) {
2754
+ console.log(` - ${decision.question}`);
2755
+ console.log(` Chose: ${decision.chosen}`);
2756
+ }
2757
+ if (group.decisions.length > 3) {
2758
+ console.log(` ... and ${group.decisions.length - 3} more`);
2759
+ }
2760
+ console.log("");
1262
2761
  }
1263
- if (compacted.keyLearnings.length > 5) {
1264
- console.log(` ... and ${compacted.keyLearnings.length - 5} more`);
2762
+ if (compacted.keyLearnings.length > 0) {
2763
+ console.log("=== Key Learnings ===\n");
2764
+ for (const learning of compacted.keyLearnings.slice(0, 5)) {
2765
+ console.log(` - ${learning}`);
2766
+ }
2767
+ if (compacted.keyLearnings.length > 5) {
2768
+ console.log(` ... and ${compacted.keyLearnings.length - 5} more`);
2769
+ }
2770
+ console.log("");
1265
2771
  }
1266
- console.log("");
1267
2772
  }
1268
2773
  if (compacted.filesAffected.length > 0) {
1269
2774
  console.log(`Files affected: ${compacted.filesAffected.length}`);
@@ -1272,7 +2777,7 @@ function printCompactedSummary(compacted) {
1272
2777
  console.log(`Commits: ${compacted.commits.length}`);
1273
2778
  }
1274
2779
  }
1275
- function formatDate2(isoString) {
2780
+ function formatDate3(isoString) {
1276
2781
  return new Date(isoString).toLocaleDateString("en-US", {
1277
2782
  month: "short",
1278
2783
  day: "numeric",
@@ -1284,12 +2789,12 @@ function capitalize(str) {
1284
2789
  }
1285
2790
 
1286
2791
  // src/cli/commands/complete.ts
1287
- import { existsSync as existsSync3 } from "fs";
2792
+ import { existsSync as existsSync4 } from "fs";
1288
2793
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
1289
- import { join as join3 } from "path";
2794
+ import { join as join5 } from "path";
1290
2795
 
1291
2796
  // src/core/trace.ts
1292
- import { execSync as execSync2 } from "child_process";
2797
+ import { execSync } from "child_process";
1293
2798
  import { createHash } from "crypto";
1294
2799
  function isValidGitRef(ref) {
1295
2800
  const validRefPattern = /^[a-zA-Z0-9_\-./]+$/;
@@ -1309,7 +2814,7 @@ function isValidGitRef(ref) {
1309
2814
  }
1310
2815
  function isGitRepo() {
1311
2816
  try {
1312
- execSync2("git rev-parse --is-inside-work-tree", {
2817
+ execSync("git rev-parse --is-inside-work-tree", {
1313
2818
  encoding: "utf-8",
1314
2819
  stdio: ["pipe", "pipe", "pipe"]
1315
2820
  });
@@ -1323,7 +2828,7 @@ function getGitHead() {
1323
2828
  return null;
1324
2829
  }
1325
2830
  try {
1326
- const head = execSync2("git rev-parse HEAD", {
2831
+ const head = execSync("git rev-parse HEAD", {
1327
2832
  encoding: "utf-8",
1328
2833
  stdio: ["pipe", "pipe", "pipe"]
1329
2834
  }).trim();
@@ -1375,7 +2880,7 @@ function getChangedFiles(startRef, endRef = "HEAD") {
1375
2880
  return [];
1376
2881
  }
1377
2882
  try {
1378
- const diffOutput = execSync2(`git diff ${startRef}..${endRef}`, {
2883
+ const diffOutput = execSync(`git diff ${startRef}..${endRef}`, {
1379
2884
  encoding: "utf-8",
1380
2885
  stdio: ["pipe", "pipe", "pipe"],
1381
2886
  maxBuffer: 10 * 1024 * 1024
@@ -1416,8 +2921,8 @@ function generateTrace(trajectory, startRef) {
1416
2921
  return null;
1417
2922
  }
1418
2923
  const model = detectModel();
1419
- const traceFiles = changedFiles.map(({ path, ranges }) => ({
1420
- path,
2924
+ const traceFiles = changedFiles.map(({ path: path2, ranges }) => ({
2925
+ path: path2,
1421
2926
  conversations: [
1422
2927
  {
1423
2928
  contributor: {
@@ -1480,8 +2985,8 @@ function createTraceRef(startRef, traceId) {
1480
2985
  }
1481
2986
 
1482
2987
  // src/core/trailers.ts
1483
- import { execSync as execSync3 } from "child_process";
1484
- import { readFileSync as readFileSync2 } from "fs";
2988
+ import { execSync as execSync2 } from "child_process";
2989
+ import { readFileSync as readFileSync3 } from "fs";
1485
2990
  function getCommitsBetween(startRef, endRef = "HEAD") {
1486
2991
  if (!isGitRepo()) {
1487
2992
  return [];
@@ -1490,7 +2995,7 @@ function getCommitsBetween(startRef, endRef = "HEAD") {
1490
2995
  return [];
1491
2996
  }
1492
2997
  try {
1493
- const output = execSync3(
2998
+ const output = execSync2(
1494
2999
  `git log --format=%H%n%h%n%s%n%an%n%aI%n--- ${startRef}..${endRef}`,
1495
3000
  {
1496
3001
  encoding: "utf-8",
@@ -1527,7 +3032,7 @@ function getFilesChangedBetween(startRef, endRef = "HEAD") {
1527
3032
  return [];
1528
3033
  }
1529
3034
  try {
1530
- const output = execSync3(`git diff --name-only ${startRef}..${endRef}`, {
3035
+ const output = execSync2(`git diff --name-only ${startRef}..${endRef}`, {
1531
3036
  encoding: "utf-8",
1532
3037
  stdio: ["pipe", "pipe", "pipe"]
1533
3038
  });
@@ -1564,8 +3069,11 @@ if [ -z "$ACTIVE_FILE" ]; then
1564
3069
  exit 0
1565
3070
  fi
1566
3071
 
1567
- # Extract trajectory ID (grep for the "id" field)
1568
- TRAJ_ID=$(grep -o '"id"[[:space:]]*:[[:space:]]*"traj_[a-z0-9]*"' "$ACTIVE_FILE" | head -1 | grep -o 'traj_[a-z0-9]*')
3072
+ # Extract trajectory ID (grep for the "id" field). Character class must
3073
+ # include underscore to match legacy traj_<timestamp>_<hex> ids -- without
3074
+ # it, grep -o silently truncates at the first internal underscore and
3075
+ # emits a wrong (shorter) id into the commit trailer.
3076
+ TRAJ_ID=$(grep -o '"id"[[:space:]]*:[[:space:]]*"traj_[a-z0-9_]*"' "$ACTIVE_FILE" | head -1 | grep -o 'traj_[a-z0-9_]*')
1569
3077
  if [ -z "$TRAJ_ID" ]; then
1570
3078
  exit 0
1571
3079
  fi
@@ -1585,13 +3093,13 @@ function detectExistingHook() {
1585
3093
  return "none";
1586
3094
  }
1587
3095
  try {
1588
- const hooksDir = execSync3("git rev-parse --git-dir", {
3096
+ const hooksDir = execSync2("git rev-parse --git-dir", {
1589
3097
  encoding: "utf-8",
1590
3098
  stdio: ["pipe", "pipe", "pipe"]
1591
3099
  }).trim();
1592
3100
  const hookPath = `${hooksDir}/hooks/prepare-commit-msg`;
1593
3101
  try {
1594
- const content = readFileSync2(hookPath, "utf-8");
3102
+ const content = readFileSync3(hookPath, "utf-8");
1595
3103
  if (content.includes("agent-trajectories")) {
1596
3104
  return "ours";
1597
3105
  }
@@ -1607,17 +3115,17 @@ function detectExistingHook() {
1607
3115
  // src/cli/commands/complete.ts
1608
3116
  async function saveTraceFile(trajectory, trace) {
1609
3117
  const dataDir = process.env.TRAJECTORIES_DATA_DIR;
1610
- const baseDir = dataDir ? dataDir : join3(process.cwd(), ".trajectories");
1611
- const completedDir = join3(baseDir, "completed");
3118
+ const baseDir = dataDir ? dataDir : join5(process.cwd(), ".trajectories");
3119
+ const completedDir = join5(baseDir, "completed");
1612
3120
  const date = new Date(trajectory.completedAt ?? trajectory.startedAt);
1613
- const monthDir = join3(
3121
+ const monthDir = join5(
1614
3122
  completedDir,
1615
3123
  `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`
1616
3124
  );
1617
- if (!existsSync3(monthDir)) {
3125
+ if (!existsSync4(monthDir)) {
1618
3126
  await mkdir2(monthDir, { recursive: true });
1619
3127
  }
1620
- const tracePath = join3(monthDir, `${trajectory.id}.trace.json`);
3128
+ const tracePath = join5(monthDir, `${trajectory.id}.trace.json`);
1621
3129
  await writeFile2(tracePath, JSON.stringify(trace, null, 2), "utf-8");
1622
3130
  }
1623
3131
  function registerCompleteCommand(program2) {
@@ -1730,17 +3238,17 @@ function registerDecisionCommand(program2) {
1730
3238
  }
1731
3239
 
1732
3240
  // src/cli/commands/enable.ts
1733
- import { execSync as execSync4 } from "child_process";
1734
- import { existsSync as existsSync4 } from "fs";
3241
+ import { execSync as execSync3 } from "child_process";
3242
+ import { existsSync as existsSync5 } from "fs";
1735
3243
  import { chmod, mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
1736
- import { join as join4 } from "path";
3244
+ import { join as join6 } from "path";
1737
3245
  function getHooksDir() {
1738
3246
  try {
1739
- const gitDir = execSync4("git rev-parse --git-dir", {
3247
+ const gitDir = execSync3("git rev-parse --git-dir", {
1740
3248
  encoding: "utf-8",
1741
3249
  stdio: ["pipe", "pipe", "pipe"]
1742
3250
  }).trim();
1743
- return join4(gitDir, "hooks");
3251
+ return join6(gitDir, "hooks");
1744
3252
  } catch {
1745
3253
  return null;
1746
3254
  }
@@ -1758,7 +3266,7 @@ function registerEnableCommand(program2) {
1758
3266
  console.error("Error: Could not determine git hooks directory");
1759
3267
  throw new Error("Cannot find hooks directory");
1760
3268
  }
1761
- const hookPath = join4(hooksDir, "prepare-commit-msg");
3269
+ const hookPath = join6(hooksDir, "prepare-commit-msg");
1762
3270
  const existing = detectExistingHook();
1763
3271
  if (existing === "other" && !options.force) {
1764
3272
  console.error("Error: A prepare-commit-msg hook already exists");
@@ -1771,7 +3279,7 @@ function registerEnableCommand(program2) {
1771
3279
  console.log("Trajectory hook is already installed");
1772
3280
  return;
1773
3281
  }
1774
- if (!existsSync4(hooksDir)) {
3282
+ if (!existsSync5(hooksDir)) {
1775
3283
  await mkdir3(hooksDir, { recursive: true });
1776
3284
  }
1777
3285
  const hookContent = generateHookScript();
@@ -1793,7 +3301,7 @@ function registerEnableCommand(program2) {
1793
3301
  console.error("Error: Could not determine git hooks directory");
1794
3302
  throw new Error("Cannot find hooks directory");
1795
3303
  }
1796
- const hookPath = join4(hooksDir, "prepare-commit-msg");
3304
+ const hookPath = join6(hooksDir, "prepare-commit-msg");
1797
3305
  const existing = detectExistingHook();
1798
3306
  if (existing === "none") {
1799
3307
  console.log("No trajectory hook installed");
@@ -1815,7 +3323,7 @@ function registerEnableCommand(program2) {
1815
3323
  // src/cli/commands/export.ts
1816
3324
  import { exec } from "child_process";
1817
3325
  import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
1818
- import { join as join5 } from "path";
3326
+ import { join as join7 } from "path";
1819
3327
 
1820
3328
  // src/export/json.ts
1821
3329
  function exportToJSON(trajectory, options) {
@@ -2229,7 +3737,7 @@ h2 {
2229
3737
  function escapeHtml(text) {
2230
3738
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
2231
3739
  }
2232
- function formatDate3(isoDate) {
3740
+ function formatDate4(isoDate) {
2233
3741
  const date = new Date(isoDate);
2234
3742
  return date.toLocaleDateString("en-US", {
2235
3743
  month: "short",
@@ -2239,7 +3747,7 @@ function formatDate3(isoDate) {
2239
3747
  minute: "2-digit"
2240
3748
  });
2241
3749
  }
2242
- function formatDuration(startDate, endDate) {
3750
+ function formatDuration2(startDate, endDate) {
2243
3751
  const start = new Date(startDate).getTime();
2244
3752
  const end = endDate ? new Date(endDate).getTime() : Date.now();
2245
3753
  const ms = end - start;
@@ -2278,7 +3786,7 @@ function renderDecision(decision) {
2278
3786
  `;
2279
3787
  }
2280
3788
  function renderEvent(event) {
2281
- const time = formatDate3(new Date(event.ts).toISOString());
3789
+ const time = formatDate4(new Date(event.ts).toISOString());
2282
3790
  let content = "";
2283
3791
  let typeClass = "";
2284
3792
  const rawData = event.raw;
@@ -2324,7 +3832,7 @@ function renderEvent(event) {
2324
3832
  </div>
2325
3833
  `;
2326
3834
  }
2327
- function renderChapter(chapter, index) {
3835
+ function renderChapter2(chapter, index) {
2328
3836
  const events = chapter.events.map(renderEvent).join("");
2329
3837
  return `
2330
3838
  <div class="chapter">
@@ -2341,7 +3849,7 @@ function renderChapter(chapter, index) {
2341
3849
  </div>
2342
3850
  `;
2343
3851
  }
2344
- function renderRetrospective(trajectory) {
3852
+ function renderRetrospective2(trajectory) {
2345
3853
  if (!trajectory.retrospective) {
2346
3854
  return "";
2347
3855
  }
@@ -2373,7 +3881,7 @@ function renderRetrospective(trajectory) {
2373
3881
  }
2374
3882
  function generateTrajectoryHtml(trajectory) {
2375
3883
  const statusClass = getStatusClass(trajectory.status);
2376
- const duration = formatDuration(trajectory.startedAt, trajectory.completedAt);
3884
+ const duration = formatDuration2(trajectory.startedAt, trajectory.completedAt);
2377
3885
  const decisions = trajectory.chapters.flatMap(
2378
3886
  (ch) => ch.events.filter((e) => e.type === "decision" && e.raw).map((e) => e.raw).filter(
2379
3887
  (d) => d !== void 0 && typeof d.question === "string"
@@ -2392,7 +3900,7 @@ function generateTrajectoryHtml(trajectory) {
2392
3900
  Chapters (${trajectory.chapters.length})
2393
3901
  </h2>
2394
3902
  <div class="collapsible-content">
2395
- ${trajectory.chapters.map(renderChapter).join("")}
3903
+ ${trajectory.chapters.map(renderChapter2).join("")}
2396
3904
  </div>
2397
3905
  ` : "";
2398
3906
  const filesHtml = trajectory.filesChanged.length ? `
@@ -2433,7 +3941,7 @@ function generateTrajectoryHtml(trajectory) {
2433
3941
  </div>
2434
3942
  <div class="meta-item">
2435
3943
  <span class="meta-label">Started</span>
2436
- <span class="meta-value">${formatDate3(trajectory.startedAt)}</span>
3944
+ <span class="meta-value">${formatDate4(trajectory.startedAt)}</span>
2437
3945
  </div>
2438
3946
  ${trajectory.task.source ? `
2439
3947
  <div class="meta-item">
@@ -2448,7 +3956,7 @@ function generateTrajectoryHtml(trajectory) {
2448
3956
  </div>
2449
3957
  </div>
2450
3958
 
2451
- ${renderRetrospective(trajectory)}
3959
+ ${renderRetrospective2(trajectory)}
2452
3960
  ${decisionsHtml}
2453
3961
  ${chaptersHtml}
2454
3962
  ${filesHtml}
@@ -2512,9 +4020,9 @@ function registerExportCommand(program2) {
2512
4020
  openInBrowser(options.output);
2513
4021
  }
2514
4022
  } else if (options.open && options.format === "html") {
2515
- const outputDir = join5(process.cwd(), ".trajectories", "html");
4023
+ const outputDir = join7(process.cwd(), ".trajectories", "html");
2516
4024
  await mkdir4(outputDir, { recursive: true });
2517
- const filePath = join5(outputDir, `${trajectory.id}.html`);
4025
+ const filePath = join7(outputDir, `${trajectory.id}.html`);
2518
4026
  await writeFile4(filePath, output, "utf-8");
2519
4027
  console.log(`\u2713 Generated: ${filePath}`);
2520
4028
  openInBrowser(filePath);
@@ -2523,25 +4031,25 @@ function registerExportCommand(program2) {
2523
4031
  }
2524
4032
  });
2525
4033
  }
2526
- function openInBrowser(path) {
4034
+ function openInBrowser(path2) {
2527
4035
  const platform = process.platform;
2528
4036
  let command;
2529
4037
  if (platform === "darwin") {
2530
- command = `open "${path}"`;
4038
+ command = `open "${path2}"`;
2531
4039
  } else if (platform === "win32") {
2532
- command = `start "" "${path}"`;
4040
+ command = `start "" "${path2}"`;
2533
4041
  } else {
2534
- command = `xdg-open "${path}"`;
4042
+ command = `xdg-open "${path2}"`;
2535
4043
  }
2536
4044
  exec(command, (error) => {
2537
4045
  if (error) {
2538
- console.log(`Open manually: file://${path}`);
4046
+ console.log(`Open manually: file://${path2}`);
2539
4047
  }
2540
4048
  });
2541
4049
  }
2542
4050
 
2543
4051
  // src/cli/commands/list.ts
2544
- import { existsSync as existsSync5 } from "fs";
4052
+ import { existsSync as existsSync6 } from "fs";
2545
4053
  function registerListCommand(program2) {
2546
4054
  program2.command("list").description("List and search trajectories").option(
2547
4055
  "-s, --status <status>",
@@ -2551,7 +4059,7 @@ function registerListCommand(program2) {
2551
4059
  let allTrajectories = [];
2552
4060
  const seenIds = /* @__PURE__ */ new Set();
2553
4061
  for (const searchPath of searchPaths) {
2554
- if (!existsSync5(searchPath)) {
4062
+ if (!existsSync6(searchPath)) {
2555
4063
  continue;
2556
4064
  }
2557
4065
  const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
@@ -2610,9 +4118,9 @@ function registerListCommand(program2) {
2610
4118
  const confidence = traj.confidence ? ` (${Math.round(traj.confidence * 100)}%)` : "";
2611
4119
  console.log(`${statusIcon} ${traj.id}`);
2612
4120
  console.log(` ${traj.title}${confidence}`);
2613
- console.log(` Started: ${formatDate4(traj.startedAt)}`);
4121
+ console.log(` Started: ${formatDate5(traj.startedAt)}`);
2614
4122
  if (traj.completedAt) {
2615
- console.log(` Completed: ${formatDate4(traj.completedAt)}`);
4123
+ console.log(` Completed: ${formatDate5(traj.completedAt)}`);
2616
4124
  }
2617
4125
  console.log("");
2618
4126
  }
@@ -2630,7 +4138,7 @@ function getStatusIcon(status) {
2630
4138
  return "\u2022";
2631
4139
  }
2632
4140
  }
2633
- function formatDate4(isoString) {
4141
+ function formatDate5(isoString) {
2634
4142
  return new Date(isoString).toLocaleDateString("en-US", {
2635
4143
  month: "short",
2636
4144
  day: "numeric",
@@ -2700,13 +4208,13 @@ function registerReflectCommand(program2) {
2700
4208
  }
2701
4209
 
2702
4210
  // src/cli/commands/show.ts
2703
- import { existsSync as existsSync6 } from "fs";
4211
+ import { existsSync as existsSync7 } from "fs";
2704
4212
  import { readFile as readFile2 } from "fs/promises";
2705
- import { join as join6 } from "path";
4213
+ import { join as join8 } from "path";
2706
4214
  async function findTrajectory(id) {
2707
4215
  const searchPaths = getSearchPaths();
2708
4216
  for (const searchPath of searchPaths) {
2709
- if (!existsSync6(searchPath)) {
4217
+ if (!existsSync7(searchPath)) {
2710
4218
  continue;
2711
4219
  }
2712
4220
  const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
@@ -2731,19 +4239,19 @@ async function findTrajectory(id) {
2731
4239
  async function findTraceFile(id) {
2732
4240
  const searchPaths = getSearchPaths();
2733
4241
  for (const searchPath of searchPaths) {
2734
- if (!existsSync6(searchPath)) {
4242
+ if (!existsSync7(searchPath)) {
2735
4243
  continue;
2736
4244
  }
2737
- const completedDir = join6(searchPath, "completed");
2738
- if (!existsSync6(completedDir)) {
4245
+ const completedDir = join8(searchPath, "completed");
4246
+ if (!existsSync7(completedDir)) {
2739
4247
  continue;
2740
4248
  }
2741
4249
  try {
2742
4250
  const { readdirSync } = await import("fs");
2743
4251
  const months = readdirSync(completedDir);
2744
4252
  for (const month of months) {
2745
- const tracePath = join6(completedDir, month, `${id}.trace.json`);
2746
- if (existsSync6(tracePath)) {
4253
+ const tracePath = join8(completedDir, month, `${id}.trace.json`);
4254
+ if (existsSync7(tracePath)) {
2747
4255
  let record;
2748
4256
  let migrated;
2749
4257
  try {
@@ -2894,7 +4402,10 @@ function registerStartCommand(program2) {
2894
4402
  program2.command("start <title>").description("Start a new trajectory").option("-t, --task <id>", "External task ID").option(
2895
4403
  "-s, --source <system>",
2896
4404
  "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) => {
4405
+ ).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(
4406
+ "-w, --workflow <id>",
4407
+ "Workflow run id (or set TRAJECTORIES_WORKFLOW_ID). Stamped onto the trajectory so `trail compact --workflow <id>` can collate a run."
4408
+ ).option("-q, --quiet", "Only output trajectory ID (for scripting)").action(async (title, options) => {
2898
4409
  const storage = new FileStorage();
2899
4410
  await storage.initialize();
2900
4411
  const active = await storage.getActive();
@@ -2917,12 +4428,16 @@ function registerStartCommand(program2) {
2917
4428
  }
2918
4429
  const agentName = options.agent ?? process.env.TRAJECTORIES_AGENT ?? void 0;
2919
4430
  const projectId = options.project ?? process.env.TRAJECTORIES_PROJECT ?? void 0;
4431
+ 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
4432
  const startRef = captureGitState();
2921
4433
  let trajectory = createTrajectory({
2922
4434
  title,
2923
4435
  source,
2924
4436
  projectId
2925
4437
  });
4438
+ if (workflowId) {
4439
+ trajectory = { ...trajectory, workflowId };
4440
+ }
2926
4441
  if (startRef) {
2927
4442
  trajectory = {
2928
4443
  ...trajectory,
@@ -2965,7 +4480,7 @@ function registerStatusCommand(program2) {
2965
4480
  console.log('Start one with: trail start "Task description"');
2966
4481
  return;
2967
4482
  }
2968
- const duration = formatDuration2(
4483
+ const duration = formatDuration3(
2969
4484
  (/* @__PURE__ */ new Date()).getTime() - new Date(active.startedAt).getTime()
2970
4485
  );
2971
4486
  const eventCount = active.chapters.reduce(
@@ -3007,7 +4522,7 @@ Current Chapter: ${currentChapter.title}`);
3007
4522
  }
3008
4523
  });
3009
4524
  }
3010
- function formatDuration2(ms) {
4525
+ function formatDuration3(ms) {
3011
4526
  const seconds = Math.floor(ms / 1e3);
3012
4527
  const minutes = Math.floor(seconds / 60);
3013
4528
  const hours = Math.floor(minutes / 60);
@@ -3033,8 +4548,37 @@ function registerCommands(program2) {
3033
4548
  registerCompactCommand(program2);
3034
4549
  }
3035
4550
 
4551
+ // src/cli/version.ts
4552
+ import fs from "fs";
4553
+ import path from "path";
4554
+ import { fileURLToPath } from "url";
4555
+ function findPackageJson(startDir) {
4556
+ let dir = startDir;
4557
+ while (dir !== path.dirname(dir)) {
4558
+ const candidate = path.join(dir, "package.json");
4559
+ if (fs.existsSync(candidate)) {
4560
+ return candidate;
4561
+ }
4562
+ dir = path.dirname(dir);
4563
+ }
4564
+ throw new Error("Could not find package.json");
4565
+ }
4566
+ function resolveCliVersion() {
4567
+ try {
4568
+ const here = path.dirname(fileURLToPath(import.meta.url));
4569
+ const packageJsonPath = findPackageJson(here);
4570
+ const packageJson = JSON.parse(
4571
+ fs.readFileSync(packageJsonPath, "utf-8")
4572
+ );
4573
+ return packageJson.version ?? "unknown";
4574
+ } catch {
4575
+ return "unknown";
4576
+ }
4577
+ }
4578
+ var VERSION = resolveCliVersion();
4579
+
3036
4580
  // src/cli/index.ts
3037
- program.name("trail").description("Leave a trail of your work for others to follow").version("0.1.0").option(
4581
+ program.name("trail").description("Leave a trail of your work for others to follow").version(VERSION).option(
3038
4582
  "--data-dir <path>",
3039
4583
  "Override trajectory storage directory (or set TRAJECTORIES_DATA_DIR)"
3040
4584
  ).hook("preAction", (thisCommand) => {