agent-trajectories 0.5.8 → 0.5.9

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
@@ -404,17 +404,23 @@ function abandonTrajectory(trajectory, reason) {
404
404
  }
405
405
 
406
406
  // src/storage/file.ts
407
- import { randomUUID } from "crypto";
408
407
  import { existsSync } from "fs";
409
408
  import {
410
409
  mkdir,
411
410
  readFile,
412
411
  readdir,
413
412
  rename,
414
- unlink,
413
+ rm,
415
414
  writeFile
416
415
  } from "fs/promises";
417
- import { basename, dirname, isAbsolute, join, relative } from "path";
416
+ import {
417
+ basename,
418
+ dirname,
419
+ isAbsolute,
420
+ join,
421
+ relative,
422
+ resolve
423
+ } from "path";
418
424
 
419
425
  // src/export/markdown.ts
420
426
  function exportToMarkdown(trajectory) {
@@ -577,6 +583,10 @@ function extractDecisions(trajectory) {
577
583
  }
578
584
 
579
585
  // src/storage/file.ts
586
+ var TRAJECTORY_FILE = "trajectory.json";
587
+ var SUMMARY_FILE = "summary.md";
588
+ var COMPACTION_FILE = "compaction.json";
589
+ var LEGACY_COMPACTION_SUFFIX = ".compaction.json";
580
590
  function expandPath(path2) {
581
591
  if (path2.startsWith("~")) {
582
592
  return join(process.env.HOME ?? "", path2.slice(1));
@@ -608,22 +618,11 @@ function describeReadFailure(reason, error) {
608
618
  if (error instanceof Error) return error.message;
609
619
  return String(error);
610
620
  }
611
- var indexLocks = /* @__PURE__ */ new Map();
612
- function withIndexLock(path2, task) {
613
- const prev = indexLocks.get(path2) ?? Promise.resolve();
614
- const next = prev.then(task, task);
615
- indexLocks.set(
616
- path2,
617
- next.catch(() => void 0)
618
- );
619
- return next;
620
- }
621
621
  var FileStorage = class {
622
622
  baseDir;
623
623
  trajectoriesDir;
624
624
  activeDir;
625
625
  completedDir;
626
- indexPath;
627
626
  lastReconcileSummary;
628
627
  constructor(baseDir) {
629
628
  this.baseDir = baseDir ?? process.cwd();
@@ -635,7 +634,6 @@ var FileStorage = class {
635
634
  }
636
635
  this.activeDir = join(this.trajectoriesDir, "active");
637
636
  this.completedDir = join(this.trajectoriesDir, "completed");
638
- this.indexPath = join(this.trajectoriesDir, "index.json");
639
637
  }
640
638
  /**
641
639
  * Initialize storage directories
@@ -644,28 +642,22 @@ var FileStorage = class {
644
642
  await mkdir(this.trajectoriesDir, { recursive: true });
645
643
  await mkdir(this.activeDir, { recursive: true });
646
644
  await mkdir(this.completedDir, { recursive: true });
647
- if (!existsSync(this.indexPath)) {
648
- await withIndexLock(this.indexPath, async () => {
649
- if (!existsSync(this.indexPath)) {
650
- await this.saveIndex(this.emptyIndex());
651
- }
652
- });
653
- }
645
+ await this.migrateLegacyIndexCompactionMarkers();
646
+ await rm(join(this.trajectoriesDir, "index.json"), { force: true });
654
647
  await this.reconcileIndex();
655
648
  }
656
649
  /**
657
- * Scan active/ and completed/ recursively and add any trajectory files
658
- * missing from the index. Existing entries are preserved reconcile
659
- * only adds, never removes.
650
+ * Scan active/ and completed/ recursively and report trajectory files
651
+ * that can be loaded plus files that should be surfaced by doctor.
660
652
  *
661
653
  * Handles three on-disk layouts in completed/:
662
654
  * - flat: completed/{id}.json (legacy workforce data)
663
- * - monthly: completed/YYYY-MM/{id}.json (current save() writes)
655
+ * - monthly: completed/YYYY-MM/{id}.json (legacy monthly layout)
656
+ * - directory: completed/YYYY-MM/{id}/trajectory.json (current layout)
664
657
  * - nested: completed/.../{id}.json (defensive — any depth)
665
658
  *
666
- * Returns a ReconcileSummary so tests and CLI wrappers can observe
667
- * outcomes without parsing logs. Only writes the index if anything was
668
- * added.
659
+ * The method name is kept for callers such as `trail doctor`, but no
660
+ * shared index file is written.
669
661
  */
670
662
  async reconcileIndex() {
671
663
  const summary = {
@@ -677,58 +669,29 @@ var FileStorage = class {
677
669
  skippedIoError: 0,
678
670
  failures: []
679
671
  };
680
- await withIndexLock(this.indexPath, async () => {
681
- const index = await this.loadIndex();
682
- const before = Object.keys(index.trajectories).length;
683
- const discovered = [];
684
- try {
685
- const activeFiles = await readdir(this.activeDir);
686
- for (const file of activeFiles) {
687
- if (!file.endsWith(".json")) continue;
688
- discovered.push(join(this.activeDir, file));
689
- }
690
- } catch (error) {
691
- if (error.code !== "ENOENT") throw error;
692
- }
693
- await this.walkJsonFilesInto(this.completedDir, discovered);
694
- for (const filePath of discovered) {
695
- summary.scanned += 1;
696
- const result = await this.readTrajectoryFile(filePath);
697
- if (!result.ok) {
698
- if (result.reason === "malformed_json") {
699
- summary.skippedMalformedJson += 1;
700
- } else if (result.reason === "schema_violation") {
701
- summary.skippedSchemaViolation += 1;
702
- } else {
703
- summary.skippedIoError += 1;
704
- }
705
- summary.failures.push({
706
- path: result.path,
707
- reason: result.reason,
708
- message: describeReadFailure(result.reason, result.error)
709
- });
710
- continue;
711
- }
712
- const trajectory = result.trajectory;
713
- if (index.trajectories[trajectory.id]) {
714
- summary.alreadyIndexed += 1;
715
- continue;
672
+ const discovered = await this.listTrajectoryFiles();
673
+ for (const filePath of discovered) {
674
+ summary.scanned += 1;
675
+ const result = await this.readTrajectoryFile(filePath);
676
+ if (!result.ok) {
677
+ if (result.reason === "malformed_json") {
678
+ summary.skippedMalformedJson += 1;
679
+ } else if (result.reason === "schema_violation") {
680
+ summary.skippedSchemaViolation += 1;
681
+ } else {
682
+ summary.skippedIoError += 1;
716
683
  }
717
- index.trajectories[trajectory.id] = {
718
- title: trajectory.task.title,
719
- status: trajectory.status,
720
- startedAt: trajectory.startedAt,
721
- completedAt: trajectory.completedAt,
722
- path: filePath
723
- };
724
- summary.added += 1;
725
- }
726
- if (Object.keys(index.trajectories).length !== before) {
727
- await this.saveIndex(index);
684
+ summary.failures.push({
685
+ path: result.path,
686
+ reason: result.reason,
687
+ message: describeReadFailure(result.reason, result.error)
688
+ });
689
+ continue;
728
690
  }
729
- });
691
+ summary.added += 1;
692
+ }
730
693
  const hadSkips = summary.skippedMalformedJson + summary.skippedSchemaViolation + summary.skippedIoError > 0;
731
- if (summary.added > 0 || hadSkips) {
694
+ if (hadSkips) {
732
695
  const parts = [`reconciled ${summary.added}/${summary.scanned}`];
733
696
  if (summary.skippedMalformedJson > 0) {
734
697
  parts.push(`malformed: ${summary.skippedMalformedJson}`);
@@ -810,7 +773,7 @@ var FileStorage = class {
810
773
  return dest;
811
774
  }
812
775
  /**
813
- * Recursively collect all .json file paths under `dir` into `out`.
776
+ * Recursively collect trajectory JSON file paths under `dir` into `out`.
814
777
  * Silently treats a missing directory as empty.
815
778
  */
816
779
  async walkJsonFilesInto(dir, out) {
@@ -825,7 +788,7 @@ var FileStorage = class {
825
788
  const entryPath = join(dir, entry.name);
826
789
  if (entry.isDirectory()) {
827
790
  await this.walkJsonFilesInto(entryPath, out);
828
- } else if (entry.isFile() && entry.name.endsWith(".json")) {
791
+ } else if (entry.isFile() && isTrajectoryJsonFile(entry.name)) {
829
792
  out.push(entryPath);
830
793
  }
831
794
  }
@@ -849,56 +812,43 @@ var FileStorage = class {
849
812
  }
850
813
  const trajectory = validation.data;
851
814
  const isCompleted = trajectory.status === "completed" || trajectory.status === "abandoned";
852
- let filePath;
815
+ const existingPaths = await this.findTrajectoryFilePaths(trajectory.id);
816
+ let trajectoryDir;
853
817
  if (isCompleted) {
854
818
  const date = new Date(trajectory.completedAt ?? trajectory.startedAt);
855
819
  const monthDir = join(
856
820
  this.completedDir,
857
821
  `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`
858
822
  );
859
- await mkdir(monthDir, { recursive: true });
860
- filePath = join(monthDir, `${trajectory.id}.json`);
861
- const activePath = join(this.activeDir, `${trajectory.id}.json`);
862
- if (existsSync(activePath)) {
863
- await unlink(activePath);
864
- }
865
- const mdPath = join(monthDir, `${trajectory.id}.md`);
866
- const markdown = exportToMarkdown(trajectory);
867
- await writeFile(mdPath, markdown, "utf-8");
823
+ trajectoryDir = join(monthDir, trajectory.id);
868
824
  } else {
869
- filePath = join(this.activeDir, `${trajectory.id}.json`);
825
+ trajectoryDir = join(this.activeDir, trajectory.id);
826
+ }
827
+ const filePath = join(trajectoryDir, TRAJECTORY_FILE);
828
+ await this.removeTrajectoryFiles(existingPaths, filePath);
829
+ await mkdir(trajectoryDir, { recursive: true });
830
+ if (isCompleted) {
831
+ const markdown = exportToMarkdown(trajectory);
832
+ await writeFile(join(trajectoryDir, SUMMARY_FILE), markdown, "utf-8");
870
833
  }
871
834
  await writeFile(filePath, JSON.stringify(trajectory, null, 2), "utf-8");
872
- await this.updateIndex(trajectory, filePath);
873
835
  }
874
836
  /**
875
837
  * Get a trajectory by ID
876
838
  */
877
839
  async get(id) {
878
- const activePath = join(this.activeDir, `${id}.json`);
879
- if (existsSync(activePath)) {
880
- return this.readTrajectoryOrNull(activePath);
881
- }
882
- const index = await this.loadIndex();
883
- const entry = index.trajectories[id];
884
- if (entry?.path && existsSync(entry.path)) {
885
- return this.readTrajectoryOrNull(entry.path);
886
- }
887
- try {
888
- const flatPath = join(this.completedDir, `${id}.json`);
889
- if (existsSync(flatPath)) {
890
- return this.readTrajectoryOrNull(flatPath);
891
- }
892
- const months = await readdir(this.completedDir);
893
- for (const month of months) {
894
- const filePath = join(this.completedDir, month, `${id}.json`);
895
- if (existsSync(filePath)) {
896
- return this.readTrajectoryOrNull(filePath);
897
- }
840
+ for (const filePath of this.getActiveCandidatePaths(id)) {
841
+ if (!existsSync(filePath)) continue;
842
+ const trajectory = await this.readTrajectoryOrNull(filePath);
843
+ if (trajectory?.id === id) {
844
+ return trajectory;
898
845
  }
899
- } catch (error) {
900
- if (error.code !== "ENOENT") {
901
- console.error("Error searching completed trajectories:", error);
846
+ }
847
+ const paths = await this.findTrajectoryFilePaths(id);
848
+ for (const filePath of paths) {
849
+ const trajectory = await this.readTrajectoryOrNull(filePath);
850
+ if (trajectory?.id === id) {
851
+ return trajectory;
902
852
  }
903
853
  }
904
854
  return null;
@@ -907,107 +857,61 @@ var FileStorage = class {
907
857
  * Get the currently active trajectory
908
858
  */
909
859
  async getActive() {
910
- try {
911
- const files = await readdir(this.activeDir);
912
- const jsonFiles = files.filter((f) => f.endsWith(".json"));
913
- if (jsonFiles.length === 0) {
914
- return null;
915
- }
916
- let mostRecent = null;
917
- let mostRecentTime = 0;
918
- for (const file of jsonFiles) {
919
- const trajectory = await this.readTrajectoryOrNull(
920
- join(this.activeDir, file)
921
- );
922
- if (trajectory) {
923
- const startTime = new Date(trajectory.startedAt).getTime();
924
- if (startTime > mostRecentTime) {
925
- mostRecentTime = startTime;
926
- mostRecent = trajectory;
927
- }
928
- }
929
- }
930
- return mostRecent;
931
- } catch (error) {
932
- if (error.code === "ENOENT") {
933
- return null;
934
- }
935
- console.error("Error reading active trajectories:", error);
860
+ const activeFiles = await this.collectTrajectoryFiles(this.activeDir);
861
+ if (activeFiles.length === 0) {
936
862
  return null;
937
863
  }
864
+ let mostRecent = null;
865
+ let mostRecentTime = 0;
866
+ for (const filePath of activeFiles) {
867
+ const trajectory = await this.readTrajectoryOrNull(filePath);
868
+ if (trajectory?.status !== "active") continue;
869
+ const startTime = new Date(trajectory.startedAt).getTime();
870
+ if (startTime > mostRecentTime) {
871
+ mostRecentTime = startTime;
872
+ mostRecent = trajectory;
873
+ }
874
+ }
875
+ return mostRecent;
938
876
  }
939
877
  /**
940
878
  * List trajectories with optional filtering
941
879
  */
942
880
  async list(query) {
943
- const index = await this.loadIndex();
944
- let entries = Object.entries(index.trajectories);
881
+ let trajectories = await this.loadAllTrajectories();
945
882
  if (query.status) {
946
- entries = entries.filter(([, entry]) => entry.status === query.status);
883
+ trajectories = trajectories.filter((t) => t.status === query.status);
947
884
  }
948
885
  if (query.since) {
949
886
  const sinceTime = new Date(query.since).getTime();
950
- entries = entries.filter(
951
- ([, entry]) => new Date(entry.startedAt).getTime() >= sinceTime
887
+ trajectories = trajectories.filter(
888
+ (trajectory) => new Date(trajectory.startedAt).getTime() >= sinceTime
952
889
  );
953
890
  }
954
891
  if (query.until) {
955
892
  const untilTime = new Date(query.until).getTime();
956
- entries = entries.filter(
957
- ([, entry]) => new Date(entry.startedAt).getTime() <= untilTime
893
+ trajectories = trajectories.filter(
894
+ (trajectory) => new Date(trajectory.startedAt).getTime() <= untilTime
958
895
  );
959
896
  }
960
897
  const sortBy = query.sortBy ?? "startedAt";
961
898
  const sortOrder = query.sortOrder ?? "desc";
962
- entries.sort((a, b) => {
963
- const aVal = a[1][sortBy] ?? "";
964
- const bVal = b[1][sortBy] ?? "";
899
+ trajectories.sort((a, b) => {
900
+ const aVal = this.getSortValue(a, sortBy);
901
+ const bVal = this.getSortValue(b, sortBy);
965
902
  const cmp = String(aVal).localeCompare(String(bVal));
966
903
  return sortOrder === "asc" ? cmp : -cmp;
967
904
  });
968
905
  const offset = query.offset ?? 0;
969
906
  const limit = query.limit ?? 500;
970
- entries = entries.slice(offset, offset + limit);
971
- return Promise.all(
972
- entries.map(async ([id, entry]) => {
973
- const trajectory = await this.get(id);
974
- return {
975
- id,
976
- title: entry.title,
977
- status: entry.status,
978
- startedAt: entry.startedAt,
979
- completedAt: entry.completedAt,
980
- confidence: trajectory?.retrospective?.confidence,
981
- chapterCount: trajectory?.chapters.length ?? 0,
982
- decisionCount: trajectory?.chapters.reduce(
983
- (count, chapter) => count + chapter.events.filter((e) => e.type === "decision").length,
984
- 0
985
- ) ?? 0
986
- };
987
- })
988
- );
907
+ trajectories = trajectories.slice(offset, offset + limit);
908
+ return trajectories.map((trajectory) => this.toSummary(trajectory));
989
909
  }
990
910
  /**
991
911
  * Delete a trajectory
992
912
  */
993
913
  async delete(id) {
994
- const activePath = join(this.activeDir, `${id}.json`);
995
- if (existsSync(activePath)) {
996
- await unlink(activePath);
997
- }
998
- await withIndexLock(this.indexPath, async () => {
999
- const index = await this.loadIndex();
1000
- const entry = index.trajectories[id];
1001
- if (entry?.path && existsSync(entry.path)) {
1002
- await unlink(entry.path);
1003
- const mdPath = entry.path.replace(".json", ".md");
1004
- if (existsSync(mdPath)) {
1005
- await unlink(mdPath);
1006
- }
1007
- }
1008
- delete index.trajectories[id];
1009
- await this.saveIndex(index);
1010
- });
914
+ await this.deleteWithSummary(id);
1011
915
  }
1012
916
  /**
1013
917
  * Search trajectories by text
@@ -1040,12 +944,395 @@ var FileStorage = class {
1040
944
  }
1041
945
  return matches;
1042
946
  }
947
+ /**
948
+ * Mark a trajectory as compacted without writing to a shared index.
949
+ */
950
+ async markCompacted(id, compactedInto) {
951
+ const markedIds = await this.markCompactedMany([id], compactedInto);
952
+ return markedIds.has(id);
953
+ }
954
+ /**
955
+ * Mark multiple trajectories as compacted with one filesystem scan.
956
+ */
957
+ async markCompactedMany(ids, compactedInto) {
958
+ const pathsById = await this.findTrajectoryFilePathsForIds(ids);
959
+ const markedIds = /* @__PURE__ */ new Set();
960
+ const compactedAt = (/* @__PURE__ */ new Date()).toISOString();
961
+ const writes = [];
962
+ for (const [id, paths] of pathsById.entries()) {
963
+ if (paths.length === 0) {
964
+ continue;
965
+ }
966
+ markedIds.add(id);
967
+ const marker = {
968
+ trajectoryId: id,
969
+ compactedInto,
970
+ compactedAt
971
+ };
972
+ for (const filePath of paths) {
973
+ writes.push(
974
+ writeFile(
975
+ this.getCompactionMarkerPath(filePath, id),
976
+ JSON.stringify(marker, null, 2),
977
+ "utf-8"
978
+ )
979
+ );
980
+ }
981
+ }
982
+ await Promise.all(writes);
983
+ return markedIds;
984
+ }
985
+ /**
986
+ * Return trajectory IDs that have a per-trajectory compaction marker.
987
+ */
988
+ async getCompactedTrajectoryIds() {
989
+ const markerPaths = await this.listCompactionMarkerFiles();
990
+ const compactedIds = /* @__PURE__ */ new Set();
991
+ for (const markerPath of markerPaths) {
992
+ try {
993
+ const marker = JSON.parse(
994
+ await readFile(markerPath, "utf-8")
995
+ );
996
+ const trajectoryId = typeof marker.trajectoryId === "string" ? marker.trajectoryId : this.getTrajectoryIdFromCompactionMarkerPath(markerPath);
997
+ if (trajectoryId && typeof marker.compactedInto === "string") {
998
+ compactedIds.add(trajectoryId);
999
+ }
1000
+ } catch {
1001
+ }
1002
+ }
1003
+ return compactedIds;
1004
+ }
1005
+ /**
1006
+ * Delete a trajectory and return file counts for CLI reporting.
1007
+ */
1008
+ async deleteWithSummary(id) {
1009
+ return this.deleteManyWithSummary([id]);
1010
+ }
1011
+ /**
1012
+ * Delete multiple trajectories with one filesystem scan.
1013
+ */
1014
+ async deleteManyWithSummary(ids) {
1015
+ const summary = {
1016
+ removedTrajectories: 0,
1017
+ deletedJsonFiles: 0,
1018
+ deletedMarkdownFiles: 0,
1019
+ deletedTraceFiles: 0,
1020
+ deletedCompactionFiles: 0
1021
+ };
1022
+ const pathsById = await this.findTrajectoryFilePathsForIds(ids);
1023
+ const deletedPaths = /* @__PURE__ */ new Set();
1024
+ for (const paths of pathsById.values()) {
1025
+ for (const filePath of paths) {
1026
+ if (deletedPaths.has(filePath)) {
1027
+ continue;
1028
+ }
1029
+ deletedPaths.add(filePath);
1030
+ await this.removeTrajectoryFile(filePath, summary);
1031
+ }
1032
+ }
1033
+ return summary;
1034
+ }
1043
1035
  /**
1044
1036
  * Close storage (no-op for file storage)
1045
1037
  */
1046
1038
  async close() {
1047
1039
  }
1048
1040
  // Private helpers
1041
+ getActiveCandidatePaths(id) {
1042
+ if (!isSafeTrajectoryId(id)) {
1043
+ return [];
1044
+ }
1045
+ return [
1046
+ join(this.activeDir, id, TRAJECTORY_FILE),
1047
+ // Legacy layout from v0.5.x and earlier.
1048
+ join(this.activeDir, `${id}.json`)
1049
+ ];
1050
+ }
1051
+ async loadAllTrajectories() {
1052
+ const files = await this.listTrajectoryFiles();
1053
+ const trajectories = /* @__PURE__ */ new Map();
1054
+ for (const filePath of files) {
1055
+ const trajectory = await this.readTrajectoryOrNull(filePath);
1056
+ if (!trajectory) {
1057
+ continue;
1058
+ }
1059
+ const current = trajectories.get(trajectory.id);
1060
+ if (!current || this.isNewerTrajectory(trajectory, current)) {
1061
+ trajectories.set(trajectory.id, trajectory);
1062
+ }
1063
+ }
1064
+ return Array.from(trajectories.values());
1065
+ }
1066
+ async listTrajectoryFiles() {
1067
+ const [activeFiles, completedFiles] = await Promise.all([
1068
+ this.collectTrajectoryFiles(this.activeDir),
1069
+ this.collectTrajectoryFiles(this.completedDir)
1070
+ ]);
1071
+ return [...activeFiles, ...completedFiles];
1072
+ }
1073
+ async collectTrajectoryFiles(dir) {
1074
+ const files = [];
1075
+ await this.walkJsonFilesInto(dir, files);
1076
+ return files;
1077
+ }
1078
+ async findTrajectoryFilePaths(id) {
1079
+ const pathsById = await this.findTrajectoryFilePathsForIds([id]);
1080
+ return pathsById.get(id) ?? [];
1081
+ }
1082
+ async findTrajectoryFilePathsForIds(ids) {
1083
+ const targetIds = new Set(Array.from(ids).filter(isSafeTrajectoryId));
1084
+ const pathsById = new Map(
1085
+ Array.from(targetIds).map((id) => [id, []])
1086
+ );
1087
+ if (targetIds.size === 0) {
1088
+ return pathsById;
1089
+ }
1090
+ const allFiles = await this.listTrajectoryFiles();
1091
+ for (const filePath of allFiles) {
1092
+ const trajectoryId = this.getTrajectoryIdFromPath(filePath);
1093
+ if (!trajectoryId || !targetIds.has(trajectoryId)) {
1094
+ continue;
1095
+ }
1096
+ pathsById.get(trajectoryId)?.push(filePath);
1097
+ }
1098
+ return pathsById;
1099
+ }
1100
+ getTrajectoryIdFromPath(filePath) {
1101
+ if (basename(filePath) === TRAJECTORY_FILE) {
1102
+ const id = basename(dirname(filePath));
1103
+ return isSafeTrajectoryId(id) ? id : void 0;
1104
+ }
1105
+ const name = basename(filePath);
1106
+ if (name.endsWith(".json")) {
1107
+ const id = name.slice(0, -".json".length);
1108
+ return isSafeTrajectoryId(id) ? id : void 0;
1109
+ }
1110
+ return void 0;
1111
+ }
1112
+ async removeTrajectoryFiles(paths, exceptPath) {
1113
+ const summary = this.emptyDeleteSummary();
1114
+ for (const filePath of paths) {
1115
+ if (filePath === exceptPath) {
1116
+ continue;
1117
+ }
1118
+ await this.removeTrajectoryFile(filePath, summary);
1119
+ }
1120
+ }
1121
+ async removeTrajectoryFile(filePath, summary) {
1122
+ if (basename(filePath) === TRAJECTORY_FILE) {
1123
+ const trajectoryDir = dirname(filePath);
1124
+ await this.countDirectoryTrajectoryFiles(trajectoryDir, summary);
1125
+ await this.removeFileIfExists(
1126
+ join(dirname(trajectoryDir), `${basename(trajectoryDir)}.trace.json`),
1127
+ "trace",
1128
+ summary
1129
+ );
1130
+ await rm(trajectoryDir, { recursive: true, force: true });
1131
+ return;
1132
+ }
1133
+ await this.removeFileIfExists(filePath, "json", summary);
1134
+ await this.removeFileIfExists(
1135
+ getMarkdownOutputPath(filePath),
1136
+ "markdown",
1137
+ summary
1138
+ );
1139
+ await this.removeFileIfExists(
1140
+ getTraceOutputPath(filePath),
1141
+ "trace",
1142
+ summary
1143
+ );
1144
+ await this.removeFileIfExists(
1145
+ getLegacyCompactionMarkerPath(filePath),
1146
+ "compaction",
1147
+ summary
1148
+ );
1149
+ }
1150
+ async countDirectoryTrajectoryFiles(trajectoryDir, summary) {
1151
+ await this.countFileIfExists(
1152
+ join(trajectoryDir, TRAJECTORY_FILE),
1153
+ "json",
1154
+ summary
1155
+ );
1156
+ await this.countFileIfExists(
1157
+ join(trajectoryDir, SUMMARY_FILE),
1158
+ "markdown",
1159
+ summary
1160
+ );
1161
+ await this.countFileIfExists(
1162
+ join(trajectoryDir, `${basename(trajectoryDir)}.trace.json`),
1163
+ "trace",
1164
+ summary
1165
+ );
1166
+ await this.countFileIfExists(
1167
+ join(trajectoryDir, "trace.json"),
1168
+ "trace",
1169
+ summary
1170
+ );
1171
+ await this.countFileIfExists(
1172
+ join(trajectoryDir, COMPACTION_FILE),
1173
+ "compaction",
1174
+ summary
1175
+ );
1176
+ }
1177
+ async removeFileIfExists(path2, kind, summary) {
1178
+ if (!existsSync(path2)) {
1179
+ return;
1180
+ }
1181
+ await rm(path2, { force: true });
1182
+ this.incrementDeleteSummary(kind, summary);
1183
+ }
1184
+ async countFileIfExists(path2, kind, summary) {
1185
+ if (existsSync(path2)) {
1186
+ this.incrementDeleteSummary(kind, summary);
1187
+ }
1188
+ }
1189
+ incrementDeleteSummary(kind, summary) {
1190
+ if (kind === "json") {
1191
+ summary.deletedJsonFiles += 1;
1192
+ summary.removedTrajectories += 1;
1193
+ } else if (kind === "markdown") {
1194
+ summary.deletedMarkdownFiles += 1;
1195
+ } else if (kind === "trace") {
1196
+ summary.deletedTraceFiles += 1;
1197
+ } else {
1198
+ summary.deletedCompactionFiles += 1;
1199
+ }
1200
+ }
1201
+ emptyDeleteSummary() {
1202
+ return {
1203
+ removedTrajectories: 0,
1204
+ deletedJsonFiles: 0,
1205
+ deletedMarkdownFiles: 0,
1206
+ deletedTraceFiles: 0,
1207
+ deletedCompactionFiles: 0
1208
+ };
1209
+ }
1210
+ async listCompactionMarkerFiles() {
1211
+ const markerPaths = [];
1212
+ await this.walkFilesInto(
1213
+ this.activeDir,
1214
+ markerPaths,
1215
+ isCompactionMarkerFile
1216
+ );
1217
+ await this.walkFilesInto(
1218
+ this.completedDir,
1219
+ markerPaths,
1220
+ isCompactionMarkerFile
1221
+ );
1222
+ return markerPaths;
1223
+ }
1224
+ getCompactionMarkerPath(filePath, id) {
1225
+ if (basename(filePath) === TRAJECTORY_FILE) {
1226
+ return join(dirname(filePath), COMPACTION_FILE);
1227
+ }
1228
+ return join(dirname(filePath), `${id}${LEGACY_COMPACTION_SUFFIX}`);
1229
+ }
1230
+ getTrajectoryIdFromCompactionMarkerPath(markerPath) {
1231
+ if (basename(markerPath) === COMPACTION_FILE) {
1232
+ const id = basename(dirname(markerPath));
1233
+ return id.startsWith("traj_") ? id : void 0;
1234
+ }
1235
+ const markerName = basename(markerPath);
1236
+ return markerName.endsWith(LEGACY_COMPACTION_SUFFIX) ? markerName.slice(0, -LEGACY_COMPACTION_SUFFIX.length) : void 0;
1237
+ }
1238
+ async migrateLegacyIndexCompactionMarkers() {
1239
+ const indexPath = join(this.trajectoriesDir, "index.json");
1240
+ if (!existsSync(indexPath)) {
1241
+ return;
1242
+ }
1243
+ let parsed;
1244
+ try {
1245
+ parsed = JSON.parse(await readFile(indexPath, "utf-8"));
1246
+ } catch {
1247
+ return;
1248
+ }
1249
+ if (parsed === null || typeof parsed !== "object") {
1250
+ return;
1251
+ }
1252
+ const trajectories = parsed.trajectories;
1253
+ if (trajectories === null || typeof trajectories !== "object" || Array.isArray(trajectories)) {
1254
+ return;
1255
+ }
1256
+ await Promise.all(
1257
+ Object.entries(trajectories).map(async ([id, entry]) => {
1258
+ if (entry === null || typeof entry !== "object" || !isSafeTrajectoryId(id)) {
1259
+ return;
1260
+ }
1261
+ const compactedInto = entry.compactedInto;
1262
+ const path2 = entry.path;
1263
+ if (typeof compactedInto !== "string") {
1264
+ return;
1265
+ }
1266
+ const paths = typeof path2 === "string" && existsSync(path2) && this.isPathInsideTrajectoriesDir(path2) ? [path2] : await this.findTrajectoryFilePaths(id);
1267
+ if (paths.length === 0) return;
1268
+ const marker = {
1269
+ trajectoryId: id,
1270
+ compactedInto,
1271
+ compactedAt: (/* @__PURE__ */ new Date()).toISOString()
1272
+ };
1273
+ await Promise.all(
1274
+ paths.map(
1275
+ (filePath) => writeFile(
1276
+ this.getCompactionMarkerPath(filePath, id),
1277
+ JSON.stringify(marker, null, 2),
1278
+ "utf-8"
1279
+ )
1280
+ )
1281
+ );
1282
+ })
1283
+ );
1284
+ }
1285
+ isPathInsideTrajectoriesDir(path2) {
1286
+ const rel = relative(resolve(this.trajectoriesDir), resolve(path2));
1287
+ return Boolean(rel && !rel.startsWith("..") && !isAbsolute(rel));
1288
+ }
1289
+ async walkFilesInto(dir, out, predicate) {
1290
+ let entries;
1291
+ try {
1292
+ entries = await readdir(dir, { withFileTypes: true });
1293
+ } catch (error) {
1294
+ if (error.code === "ENOENT") return;
1295
+ throw error;
1296
+ }
1297
+ for (const entry of entries) {
1298
+ const entryPath = join(dir, entry.name);
1299
+ if (entry.isDirectory()) {
1300
+ await this.walkFilesInto(entryPath, out, predicate);
1301
+ } else if (entry.isFile() && predicate(entry.name)) {
1302
+ out.push(entryPath);
1303
+ }
1304
+ }
1305
+ }
1306
+ getSortValue(trajectory, sortBy) {
1307
+ if (sortBy === "title") {
1308
+ return trajectory.task.title;
1309
+ }
1310
+ return trajectory[sortBy] ?? "";
1311
+ }
1312
+ toSummary(trajectory) {
1313
+ return {
1314
+ id: trajectory.id,
1315
+ title: trajectory.task.title,
1316
+ status: trajectory.status,
1317
+ startedAt: trajectory.startedAt,
1318
+ completedAt: trajectory.completedAt,
1319
+ confidence: trajectory.retrospective?.confidence,
1320
+ chapterCount: trajectory.chapters.length,
1321
+ decisionCount: trajectory.chapters.reduce(
1322
+ (count, chapter) => count + chapter.events.filter((event) => event.type === "decision").length,
1323
+ 0
1324
+ )
1325
+ };
1326
+ }
1327
+ isNewerTrajectory(candidate, current) {
1328
+ const candidateTime = new Date(
1329
+ candidate.completedAt ?? candidate.startedAt
1330
+ ).getTime();
1331
+ const currentTime = new Date(
1332
+ current.completedAt ?? current.startedAt
1333
+ ).getTime();
1334
+ return candidateTime > currentTime;
1335
+ }
1049
1336
  /**
1050
1337
  * Read a trajectory file and return a tagged result so callers can
1051
1338
  * distinguish missing files, malformed JSON, and schema violations.
@@ -1085,82 +1372,25 @@ var FileStorage = class {
1085
1372
  const result = await this.readTrajectoryFile(path2);
1086
1373
  return result.ok ? result.trajectory : null;
1087
1374
  }
1088
- /**
1089
- * Read and parse the on-disk index.
1090
- *
1091
- * Tolerances (belt-and-braces against the read/write race):
1092
- * - ENOENT: first-run, return an empty index silently.
1093
- * - Empty file: a concurrent writer truncated index.json in "w" mode
1094
- * right before we read. Return an empty index silently — this is
1095
- * not a real corruption, just an interleaving the mutex + atomic
1096
- * rename should already prevent. Logging here would be noise.
1097
- * - Non-empty but malformed JSON: genuinely corrupted on disk (hand
1098
- * edit, disk error, etc). Log it and return an empty index so the
1099
- * caller can recover, but keep the log so the problem is visible.
1100
- */
1101
- async loadIndex() {
1102
- let content;
1103
- try {
1104
- content = await readFile(this.indexPath, "utf-8");
1105
- } catch (error) {
1106
- if (error.code !== "ENOENT") {
1107
- console.error(
1108
- "Error loading trajectory index, using empty index:",
1109
- error
1110
- );
1111
- }
1112
- return this.emptyIndex();
1113
- }
1114
- if (content.length === 0) {
1115
- return this.emptyIndex();
1116
- }
1117
- try {
1118
- return JSON.parse(content);
1119
- } catch (error) {
1120
- console.error(
1121
- "Error loading trajectory index, using empty index:",
1122
- error
1123
- );
1124
- return this.emptyIndex();
1125
- }
1126
- }
1127
- emptyIndex() {
1128
- return {
1129
- version: 1,
1130
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
1131
- trajectories: {}
1132
- };
1133
- }
1134
- /**
1135
- * Atomic write: stage into a process-unique temp path in the same directory
1136
- * and then rename over the live file. `rename` is atomic on POSIX, so
1137
- * concurrent readers in any process either see the old complete file or
1138
- * the new complete file — never a half-written / zero-byte state.
1139
- *
1140
- * Callers MUST hold `withIndexLock(this.indexPath, ...)` so the in-process
1141
- * read-modify-write cycle stays serialized; the unique temp name also keeps
1142
- * parallel writers in other processes from colliding on a shared tmp path.
1143
- */
1144
- async saveIndex(index) {
1145
- index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
1146
- const tmpPath = `${this.indexPath}.${process.pid}.${randomUUID()}.tmp`;
1147
- await writeFile(tmpPath, JSON.stringify(index, null, 2), "utf-8");
1148
- await rename(tmpPath, this.indexPath);
1149
- }
1150
- async updateIndex(trajectory, filePath) {
1151
- await withIndexLock(this.indexPath, async () => {
1152
- const index = await this.loadIndex();
1153
- index.trajectories[trajectory.id] = {
1154
- title: trajectory.task.title,
1155
- status: trajectory.status,
1156
- startedAt: trajectory.startedAt,
1157
- completedAt: trajectory.completedAt,
1158
- path: filePath
1159
- };
1160
- await this.saveIndex(index);
1161
- });
1162
- }
1163
1375
  };
1376
+ function isTrajectoryJsonFile(name) {
1377
+ return name === TRAJECTORY_FILE || name.endsWith(".json") && name !== "index.json" && !name.endsWith(".trace.json") && !name.endsWith(LEGACY_COMPACTION_SUFFIX) && name !== COMPACTION_FILE;
1378
+ }
1379
+ function isSafeTrajectoryId(id) {
1380
+ return id.length > 0 && !id.includes("..") && !id.includes("/") && !id.includes("\\");
1381
+ }
1382
+ function isCompactionMarkerFile(name) {
1383
+ return name === COMPACTION_FILE || name.endsWith(LEGACY_COMPACTION_SUFFIX);
1384
+ }
1385
+ function getMarkdownOutputPath(outputPath) {
1386
+ return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".md") : `${outputPath}.md`;
1387
+ }
1388
+ function getTraceOutputPath(outputPath) {
1389
+ return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".trace.json") : `${outputPath}.trace.json`;
1390
+ }
1391
+ function getLegacyCompactionMarkerPath(outputPath) {
1392
+ return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(LEGACY_COMPACTION_SUFFIX) : `${outputPath}${LEGACY_COMPACTION_SUFFIX}`;
1393
+ }
1164
1394
 
1165
1395
  // src/cli/commands/abandon.ts
1166
1396
  function registerAbandonCommand(program2) {
@@ -1183,13 +1413,7 @@ function registerAbandonCommand(program2) {
1183
1413
 
1184
1414
  // src/cli/commands/compact.ts
1185
1415
  import { execFileSync } from "child_process";
1186
- import {
1187
- existsSync as existsSync3,
1188
- mkdirSync,
1189
- readFileSync as readFileSync2,
1190
- unlinkSync,
1191
- writeFileSync
1192
- } from "fs";
1416
+ import { existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
1193
1417
  import { dirname as dirname2, join as join4 } from "path";
1194
1418
 
1195
1419
  // src/compact/config.ts
@@ -1894,7 +2118,7 @@ function buildCliArgs(cli) {
1894
2118
  }
1895
2119
  }
1896
2120
  function spawnWithStdin(command, args, input) {
1897
- return new Promise((resolve, reject) => {
2121
+ return new Promise((resolve2, reject) => {
1898
2122
  const child = spawn(command, args, {
1899
2123
  timeout: 3e5,
1900
2124
  stdio: ["pipe", "pipe", "pipe"]
@@ -1912,7 +2136,7 @@ function spawnWithStdin(command, args, input) {
1912
2136
  new Error(`CLI exited with code ${code}: ${stderr.slice(0, 200)}`)
1913
2137
  );
1914
2138
  } else {
1915
- resolve(Buffer.concat(chunks).toString().trim());
2139
+ resolve2(Buffer.concat(chunks).toString().trim());
1916
2140
  }
1917
2141
  });
1918
2142
  child.stdin.write(input);
@@ -2337,7 +2561,7 @@ function registerCompactCommand(program2) {
2337
2561
  "Comma-separated focus areas to emphasize in LLM compaction"
2338
2562
  ).option("--markdown", "Also write a Markdown companion file").option("--no-markdown", "Skip writing a Markdown companion file").option(
2339
2563
  "--discard-sources",
2340
- "After saving the compaction, delete source trajectory JSON/MD/trace files and remove their index entries"
2564
+ "After saving the compaction, delete source trajectory JSON/MD/trace files"
2341
2565
  ).option("--dry-run", "Preview what would be compacted without saving").option("--output <path>", "Output path for compacted trajectory").action(async (options) => {
2342
2566
  const trajectories = await loadTrajectories(options);
2343
2567
  if (trajectories.length === 0) {
@@ -2378,7 +2602,7 @@ function registerCompactCommand(program2) {
2378
2602
  markdownEnabled
2379
2603
  );
2380
2604
  if (options.discardSources) {
2381
- const discardSummary = discardSourceTrajectories(trajectories);
2605
+ const discardSummary = await discardSourceTrajectories(trajectories);
2382
2606
  printDiscardSummary(discardSummary);
2383
2607
  } else {
2384
2608
  await markTrajectoriesAsCompacted(
@@ -2390,7 +2614,7 @@ function registerCompactCommand(program2) {
2390
2614
  Compacted trajectory saved to: ${outputPath2}`);
2391
2615
  if (markdownEnabled) {
2392
2616
  console.log(
2393
- `Markdown summary saved to: ${getMarkdownOutputPath(outputPath2)}`
2617
+ `Markdown summary saved to: ${getMarkdownOutputPath2(outputPath2)}`
2394
2618
  );
2395
2619
  }
2396
2620
  printCompactedSummary(mechanicalCompacted);
@@ -2439,7 +2663,7 @@ Compacted trajectory saved to: ${outputPath2}`);
2439
2663
  const outputPath = options.output || getDefaultOutputPath(compacted, options.workflow);
2440
2664
  saveCompactionArtifacts(compacted, outputPath, markdownEnabled);
2441
2665
  if (options.discardSources) {
2442
- const discardSummary = discardSourceTrajectories(trajectories);
2666
+ const discardSummary = await discardSourceTrajectories(trajectories);
2443
2667
  printDiscardSummary(discardSummary);
2444
2668
  } else {
2445
2669
  await markTrajectoriesAsCompacted(trajectories, compacted.id);
@@ -2448,7 +2672,7 @@ Compacted trajectory saved to: ${outputPath2}`);
2448
2672
  Compacted trajectory saved to: ${outputPath}`);
2449
2673
  if (markdownEnabled) {
2450
2674
  console.log(
2451
- `Markdown summary saved to: ${getMarkdownOutputPath(outputPath)}`
2675
+ `Markdown summary saved to: ${getMarkdownOutputPath2(outputPath)}`
2452
2676
  );
2453
2677
  }
2454
2678
  printCompactedSummary(compacted);
@@ -2467,19 +2691,12 @@ async function loadTrajectories(options) {
2467
2691
  return [trimmed, trimmed.slice(0, 7)];
2468
2692
  })
2469
2693
  ) : null;
2470
- const compactedIds = options.all ? /* @__PURE__ */ new Set() : getCompactedTrajectoryIds();
2694
+ const compactedIds = options.all ? /* @__PURE__ */ new Set() : await getCompactedTrajectoryIds();
2471
2695
  const searchPaths = getSearchPaths();
2472
2696
  const seenIds = /* @__PURE__ */ new Set();
2473
2697
  for (const searchPath of searchPaths) {
2474
2698
  if (!existsSync3(searchPath)) continue;
2475
- const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
2476
- process.env.TRAJECTORIES_DATA_DIR = searchPath;
2477
- const storage = new FileStorage();
2478
- if (originalDataDir !== void 0) {
2479
- process.env.TRAJECTORIES_DATA_DIR = originalDataDir;
2480
- } else {
2481
- delete process.env.TRAJECTORIES_DATA_DIR;
2482
- }
2699
+ const storage = createStorageForSearchPath(searchPath);
2483
2700
  await storage.initialize();
2484
2701
  const summaries = await storage.list({
2485
2702
  status: "completed",
@@ -2525,6 +2742,19 @@ async function loadTrajectories(options) {
2525
2742
  }
2526
2743
  return trajectories;
2527
2744
  }
2745
+ function createStorageForSearchPath(searchPath) {
2746
+ const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
2747
+ process.env.TRAJECTORIES_DATA_DIR = searchPath;
2748
+ try {
2749
+ return new FileStorage();
2750
+ } finally {
2751
+ if (originalDataDir !== void 0) {
2752
+ process.env.TRAJECTORIES_DATA_DIR = originalDataDir;
2753
+ } else {
2754
+ delete process.env.TRAJECTORIES_DATA_DIR;
2755
+ }
2756
+ }
2757
+ }
2528
2758
  function getBranchCommits(targetBranch) {
2529
2759
  const commits = /* @__PURE__ */ new Set();
2530
2760
  try {
@@ -2549,21 +2779,17 @@ function getBranchCommits(targetBranch) {
2549
2779
  }
2550
2780
  return commits;
2551
2781
  }
2552
- function getCompactedTrajectoryIds() {
2782
+ async function getCompactedTrajectoryIds() {
2553
2783
  const compacted = /* @__PURE__ */ new Set();
2554
2784
  const searchPaths = getSearchPaths();
2555
2785
  for (const searchPath of searchPaths) {
2556
- const indexPath = join4(searchPath, "index.json");
2557
- if (!existsSync3(indexPath)) continue;
2558
- try {
2559
- const indexContent = readFileSync2(indexPath, "utf-8");
2560
- const index = JSON.parse(indexContent);
2561
- for (const [id, entry] of Object.entries(index.trajectories || {})) {
2562
- if (entry.compactedInto) {
2563
- compacted.add(id);
2564
- }
2565
- }
2566
- } catch {
2786
+ if (!existsSync3(searchPath)) {
2787
+ continue;
2788
+ }
2789
+ const storage = createStorageForSearchPath(searchPath);
2790
+ await storage.initialize();
2791
+ for (const id of await storage.getCompactedTrajectoryIds()) {
2792
+ compacted.add(id);
2567
2793
  }
2568
2794
  }
2569
2795
  return compacted;
@@ -2571,92 +2797,45 @@ function getCompactedTrajectoryIds() {
2571
2797
  async function markTrajectoriesAsCompacted(trajectories, compactedIntoId) {
2572
2798
  const searchPaths = getSearchPaths();
2573
2799
  for (const searchPath of searchPaths) {
2574
- const indexPath = join4(searchPath, "index.json");
2575
- if (!existsSync3(indexPath)) continue;
2576
- try {
2577
- const indexContent = readFileSync2(indexPath, "utf-8");
2578
- const index = JSON.parse(indexContent);
2579
- let updated = false;
2580
- for (const traj of trajectories) {
2581
- if (index.trajectories[traj.id]) {
2582
- index.trajectories[traj.id].compactedInto = compactedIntoId;
2583
- updated = true;
2584
- }
2585
- }
2586
- if (updated) {
2587
- index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
2588
- writeFileSync(indexPath, JSON.stringify(index, null, 2));
2589
- }
2590
- } catch {
2800
+ if (!existsSync3(searchPath)) {
2801
+ continue;
2591
2802
  }
2803
+ const storage = createStorageForSearchPath(searchPath);
2804
+ await storage.initialize();
2805
+ await storage.markCompactedMany(
2806
+ trajectories.map((trajectory) => trajectory.id),
2807
+ compactedIntoId
2808
+ );
2592
2809
  }
2593
2810
  }
2594
- function discardSourceTrajectories(trajectories) {
2595
- const sourceIds = new Set(trajectories.map((trajectory) => trajectory.id));
2811
+ async function discardSourceTrajectories(trajectories) {
2596
2812
  const summary = {
2597
- removedIndexEntries: 0,
2813
+ removedTrajectories: 0,
2598
2814
  deletedJsonFiles: 0,
2599
2815
  deletedMarkdownFiles: 0,
2600
- deletedTraceFiles: 0
2816
+ deletedTraceFiles: 0,
2817
+ deletedCompactionFiles: 0
2601
2818
  };
2602
2819
  for (const searchPath of getSearchPaths()) {
2603
- const indexPath = join4(searchPath, "index.json");
2604
- if (!existsSync3(indexPath)) continue;
2605
- let index;
2606
- try {
2607
- const indexContent = readFileSync2(indexPath, "utf-8");
2608
- const parsedIndex = JSON.parse(indexContent);
2609
- if (!isTrajectoryIndex(parsedIndex)) {
2610
- continue;
2611
- }
2612
- index = parsedIndex;
2613
- } catch {
2820
+ if (!existsSync3(searchPath)) {
2614
2821
  continue;
2615
2822
  }
2616
- let updated = false;
2617
- for (const id of sourceIds) {
2618
- const entry = index.trajectories[id];
2619
- if (!entry) continue;
2620
- if (deleteFileIfExists(entry.path)) {
2621
- summary.deletedJsonFiles += 1;
2622
- }
2623
- if (deleteFileIfExists(getMarkdownOutputPath(entry.path))) {
2624
- summary.deletedMarkdownFiles += 1;
2625
- }
2626
- if (deleteFileIfExists(getTraceOutputPath(entry.path))) {
2627
- summary.deletedTraceFiles += 1;
2628
- }
2629
- delete index.trajectories[id];
2630
- summary.removedIndexEntries += 1;
2631
- updated = true;
2632
- }
2633
- if (updated) {
2634
- index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
2635
- writeFileSync(indexPath, JSON.stringify(index, null, 2));
2636
- }
2823
+ const storage = createStorageForSearchPath(searchPath);
2824
+ await storage.initialize();
2825
+ const deleteSummary = await storage.deleteManyWithSummary(
2826
+ trajectories.map((trajectory) => trajectory.id)
2827
+ );
2828
+ summary.removedTrajectories += deleteSummary.removedTrajectories;
2829
+ summary.deletedJsonFiles += deleteSummary.deletedJsonFiles;
2830
+ summary.deletedMarkdownFiles += deleteSummary.deletedMarkdownFiles;
2831
+ summary.deletedTraceFiles += deleteSummary.deletedTraceFiles;
2832
+ summary.deletedCompactionFiles += deleteSummary.deletedCompactionFiles;
2637
2833
  }
2638
2834
  return summary;
2639
2835
  }
2640
- function deleteFileIfExists(path2) {
2641
- if (!existsSync3(path2)) {
2642
- return false;
2643
- }
2644
- unlinkSync(path2);
2645
- return true;
2646
- }
2647
- function isTrajectoryIndex(value) {
2648
- if (value === null || typeof value !== "object") {
2649
- return false;
2650
- }
2651
- const candidate = value;
2652
- return candidate.trajectories !== null && typeof candidate.trajectories === "object" && !Array.isArray(candidate.trajectories);
2653
- }
2654
- function getTraceOutputPath(outputPath) {
2655
- return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".trace.json") : `${outputPath}.trace.json`;
2656
- }
2657
2836
  function printDiscardSummary(summary) {
2658
2837
  console.log(
2659
- `Discarded source trajectories: ${summary.removedIndexEntries} index entries, ${summary.deletedJsonFiles} JSON files, ${summary.deletedMarkdownFiles} Markdown files, ${summary.deletedTraceFiles} trace files`
2838
+ `Discarded source trajectories: ${summary.removedTrajectories} trajectories, ${summary.deletedJsonFiles} JSON files, ${summary.deletedMarkdownFiles} Markdown files, ${summary.deletedTraceFiles} trace files, ${summary.deletedCompactionFiles} compaction markers`
2660
2839
  );
2661
2840
  }
2662
2841
  function parseRelativeDate(input) {
@@ -2901,12 +3080,12 @@ function saveCompactionArtifacts(compacted, outputPath, markdownEnabled) {
2901
3080
  writeFileSync(outputPath, JSON.stringify(compacted, null, 2));
2902
3081
  if (markdownEnabled) {
2903
3082
  writeFileSync(
2904
- getMarkdownOutputPath(outputPath),
3083
+ getMarkdownOutputPath2(outputPath),
2905
3084
  renderCompactionMarkdown(compacted)
2906
3085
  );
2907
3086
  }
2908
3087
  }
2909
- function getMarkdownOutputPath(outputPath) {
3088
+ function getMarkdownOutputPath2(outputPath) {
2910
3089
  return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".md") : `${outputPath}.md`;
2911
3090
  }
2912
3091
  function renderCompactionMarkdown(compacted) {
@@ -3229,7 +3408,7 @@ function createTraceRef(startRef, traceId) {
3229
3408
 
3230
3409
  // src/core/trailers.ts
3231
3410
  import { execSync as execSync2 } from "child_process";
3232
- import { readFileSync as readFileSync3 } from "fs";
3411
+ import { readFileSync as readFileSync2 } from "fs";
3233
3412
  function getCommitsBetween(startRef, endRef = "HEAD") {
3234
3413
  if (!isGitRepo()) {
3235
3414
  return [];
@@ -3342,7 +3521,7 @@ function detectExistingHook() {
3342
3521
  }).trim();
3343
3522
  const hookPath = `${hooksDir}/hooks/prepare-commit-msg`;
3344
3523
  try {
3345
- const content = readFileSync3(hookPath, "utf-8");
3524
+ const content = readFileSync2(hookPath, "utf-8");
3346
3525
  if (content.includes("agent-trajectories")) {
3347
3526
  return "ours";
3348
3527
  }
@@ -3602,8 +3781,8 @@ function registerEnableCommand(program2) {
3602
3781
  console.error("Remove it manually if needed");
3603
3782
  throw new Error("Hook not ours");
3604
3783
  }
3605
- const { unlink: unlink2 } = await import("fs/promises");
3606
- await unlink2(hookPath);
3784
+ const { unlink } = await import("fs/promises");
3785
+ await unlink(hookPath);
3607
3786
  console.log("Trajectory hook removed");
3608
3787
  });
3609
3788
  }