agent-trajectories 0.5.8 → 0.6.0

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,18 @@ 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";
590
+ var DEFAULT_TRAJECTORY_DATA_DIR = join(
591
+ ".agentworkforce",
592
+ "trajectories"
593
+ );
594
+ var LEGACY_TRAJECTORY_DATA_DIR = ".trajectories";
595
+ function getDefaultTrajectoryDataDir(baseDir = process.cwd()) {
596
+ return join(baseDir, DEFAULT_TRAJECTORY_DATA_DIR);
597
+ }
580
598
  function expandPath(path2) {
581
599
  if (path2.startsWith("~")) {
582
600
  return join(process.env.HOME ?? "", path2.slice(1));
@@ -592,7 +610,7 @@ function getSearchPaths() {
592
610
  if (dataDir) {
593
611
  return [expandPath(dataDir)];
594
612
  }
595
- return [join(process.cwd(), ".trajectories")];
613
+ return [getDefaultTrajectoryDataDir()];
596
614
  }
597
615
  function describeReadFailure(reason, error) {
598
616
  if (reason === "schema_violation" && error && typeof error === "object" && "issues" in error) {
@@ -608,64 +626,60 @@ function describeReadFailure(reason, error) {
608
626
  if (error instanceof Error) return error.message;
609
627
  return String(error);
610
628
  }
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
629
  var FileStorage = class {
622
630
  baseDir;
623
631
  trajectoriesDir;
624
632
  activeDir;
625
633
  completedDir;
626
- indexPath;
627
634
  lastReconcileSummary;
635
+ shouldMigrateLegacyDefault = false;
628
636
  constructor(baseDir) {
629
637
  this.baseDir = baseDir ?? process.cwd();
630
638
  const dataDir = process.env.TRAJECTORIES_DATA_DIR;
631
639
  if (dataDir) {
632
640
  this.trajectoriesDir = expandPath(dataDir);
633
641
  } else {
634
- this.trajectoriesDir = join(this.baseDir, ".trajectories");
642
+ this.trajectoriesDir = getDefaultTrajectoryDataDir(this.baseDir);
643
+ this.shouldMigrateLegacyDefault = true;
635
644
  }
636
645
  this.activeDir = join(this.trajectoriesDir, "active");
637
646
  this.completedDir = join(this.trajectoriesDir, "completed");
638
- this.indexPath = join(this.trajectoriesDir, "index.json");
639
647
  }
640
648
  /**
641
649
  * Initialize storage directories
642
650
  */
643
651
  async initialize() {
652
+ await this.migrateLegacyDefaultDir();
644
653
  await mkdir(this.trajectoriesDir, { recursive: true });
645
654
  await mkdir(this.activeDir, { recursive: true });
646
655
  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
- }
656
+ await this.migrateLegacyIndexCompactionMarkers();
657
+ await rm(join(this.trajectoriesDir, "index.json"), { force: true });
654
658
  await this.reconcileIndex();
655
659
  }
660
+ async migrateLegacyDefaultDir() {
661
+ if (!this.shouldMigrateLegacyDefault) {
662
+ return;
663
+ }
664
+ const legacyDir = join(this.baseDir, LEGACY_TRAJECTORY_DATA_DIR);
665
+ if (!existsSync(legacyDir) || existsSync(this.trajectoriesDir)) {
666
+ return;
667
+ }
668
+ await mkdir(dirname(this.trajectoriesDir), { recursive: true });
669
+ await rename(legacyDir, this.trajectoriesDir);
670
+ }
656
671
  /**
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.
672
+ * Scan active/ and completed/ recursively and report trajectory files
673
+ * that can be loaded plus files that should be surfaced by doctor.
660
674
  *
661
675
  * Handles three on-disk layouts in completed/:
662
676
  * - flat: completed/{id}.json (legacy workforce data)
663
- * - monthly: completed/YYYY-MM/{id}.json (current save() writes)
677
+ * - monthly: completed/YYYY-MM/{id}.json (legacy monthly layout)
678
+ * - directory: completed/YYYY-MM/{id}/trajectory.json (current layout)
664
679
  * - nested: completed/.../{id}.json (defensive — any depth)
665
680
  *
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.
681
+ * The method name is kept for callers such as `trail doctor`, but no
682
+ * shared index file is written.
669
683
  */
670
684
  async reconcileIndex() {
671
685
  const summary = {
@@ -677,58 +691,29 @@ var FileStorage = class {
677
691
  skippedIoError: 0,
678
692
  failures: []
679
693
  };
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;
694
+ const discovered = await this.listTrajectoryFiles();
695
+ for (const filePath of discovered) {
696
+ summary.scanned += 1;
697
+ const result = await this.readTrajectoryFile(filePath);
698
+ if (!result.ok) {
699
+ if (result.reason === "malformed_json") {
700
+ summary.skippedMalformedJson += 1;
701
+ } else if (result.reason === "schema_violation") {
702
+ summary.skippedSchemaViolation += 1;
703
+ } else {
704
+ summary.skippedIoError += 1;
716
705
  }
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);
706
+ summary.failures.push({
707
+ path: result.path,
708
+ reason: result.reason,
709
+ message: describeReadFailure(result.reason, result.error)
710
+ });
711
+ continue;
728
712
  }
729
- });
713
+ summary.added += 1;
714
+ }
730
715
  const hadSkips = summary.skippedMalformedJson + summary.skippedSchemaViolation + summary.skippedIoError > 0;
731
- if (summary.added > 0 || hadSkips) {
716
+ if (hadSkips) {
732
717
  const parts = [`reconciled ${summary.added}/${summary.scanned}`];
733
718
  if (summary.skippedMalformedJson > 0) {
734
719
  parts.push(`malformed: ${summary.skippedMalformedJson}`);
@@ -753,7 +738,7 @@ var FileStorage = class {
753
738
  return this.lastReconcileSummary;
754
739
  }
755
740
  /**
756
- * Move trajectory files that fail to load into `.trajectories/invalid/`
741
+ * Move trajectory files that fail to load into `.agentworkforce/trajectories/invalid/`
757
742
  * so reconcile no longer scans them. Only quarantines parse and schema
758
743
  * failures — transient io_error failures are left in place because the
759
744
  * file may load fine on the next attempt.
@@ -810,7 +795,7 @@ var FileStorage = class {
810
795
  return dest;
811
796
  }
812
797
  /**
813
- * Recursively collect all .json file paths under `dir` into `out`.
798
+ * Recursively collect trajectory JSON file paths under `dir` into `out`.
814
799
  * Silently treats a missing directory as empty.
815
800
  */
816
801
  async walkJsonFilesInto(dir, out) {
@@ -825,7 +810,7 @@ var FileStorage = class {
825
810
  const entryPath = join(dir, entry.name);
826
811
  if (entry.isDirectory()) {
827
812
  await this.walkJsonFilesInto(entryPath, out);
828
- } else if (entry.isFile() && entry.name.endsWith(".json")) {
813
+ } else if (entry.isFile() && isTrajectoryJsonFile(entry.name)) {
829
814
  out.push(entryPath);
830
815
  }
831
816
  }
@@ -849,56 +834,43 @@ var FileStorage = class {
849
834
  }
850
835
  const trajectory = validation.data;
851
836
  const isCompleted = trajectory.status === "completed" || trajectory.status === "abandoned";
852
- let filePath;
837
+ const existingPaths = await this.findTrajectoryFilePaths(trajectory.id);
838
+ let trajectoryDir;
853
839
  if (isCompleted) {
854
840
  const date = new Date(trajectory.completedAt ?? trajectory.startedAt);
855
841
  const monthDir = join(
856
842
  this.completedDir,
857
843
  `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`
858
844
  );
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");
845
+ trajectoryDir = join(monthDir, trajectory.id);
868
846
  } else {
869
- filePath = join(this.activeDir, `${trajectory.id}.json`);
847
+ trajectoryDir = join(this.activeDir, trajectory.id);
848
+ }
849
+ const filePath = join(trajectoryDir, TRAJECTORY_FILE);
850
+ await this.removeTrajectoryFiles(existingPaths, filePath);
851
+ await mkdir(trajectoryDir, { recursive: true });
852
+ if (isCompleted) {
853
+ const markdown = exportToMarkdown(trajectory);
854
+ await writeFile(join(trajectoryDir, SUMMARY_FILE), markdown, "utf-8");
870
855
  }
871
856
  await writeFile(filePath, JSON.stringify(trajectory, null, 2), "utf-8");
872
- await this.updateIndex(trajectory, filePath);
873
857
  }
874
858
  /**
875
859
  * Get a trajectory by ID
876
860
  */
877
861
  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
- }
862
+ for (const filePath of this.getActiveCandidatePaths(id)) {
863
+ if (!existsSync(filePath)) continue;
864
+ const trajectory = await this.readTrajectoryOrNull(filePath);
865
+ if (trajectory?.id === id) {
866
+ return trajectory;
898
867
  }
899
- } catch (error) {
900
- if (error.code !== "ENOENT") {
901
- console.error("Error searching completed trajectories:", error);
868
+ }
869
+ const paths = await this.findTrajectoryFilePaths(id);
870
+ for (const filePath of paths) {
871
+ const trajectory = await this.readTrajectoryOrNull(filePath);
872
+ if (trajectory?.id === id) {
873
+ return trajectory;
902
874
  }
903
875
  }
904
876
  return null;
@@ -907,107 +879,61 @@ var FileStorage = class {
907
879
  * Get the currently active trajectory
908
880
  */
909
881
  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);
882
+ const activeFiles = await this.collectTrajectoryFiles(this.activeDir);
883
+ if (activeFiles.length === 0) {
936
884
  return null;
937
885
  }
886
+ let mostRecent = null;
887
+ let mostRecentTime = 0;
888
+ for (const filePath of activeFiles) {
889
+ const trajectory = await this.readTrajectoryOrNull(filePath);
890
+ if (trajectory?.status !== "active") continue;
891
+ const startTime = new Date(trajectory.startedAt).getTime();
892
+ if (startTime > mostRecentTime) {
893
+ mostRecentTime = startTime;
894
+ mostRecent = trajectory;
895
+ }
896
+ }
897
+ return mostRecent;
938
898
  }
939
899
  /**
940
900
  * List trajectories with optional filtering
941
901
  */
942
902
  async list(query) {
943
- const index = await this.loadIndex();
944
- let entries = Object.entries(index.trajectories);
903
+ let trajectories = await this.loadAllTrajectories();
945
904
  if (query.status) {
946
- entries = entries.filter(([, entry]) => entry.status === query.status);
905
+ trajectories = trajectories.filter((t) => t.status === query.status);
947
906
  }
948
907
  if (query.since) {
949
908
  const sinceTime = new Date(query.since).getTime();
950
- entries = entries.filter(
951
- ([, entry]) => new Date(entry.startedAt).getTime() >= sinceTime
909
+ trajectories = trajectories.filter(
910
+ (trajectory) => new Date(trajectory.startedAt).getTime() >= sinceTime
952
911
  );
953
912
  }
954
913
  if (query.until) {
955
914
  const untilTime = new Date(query.until).getTime();
956
- entries = entries.filter(
957
- ([, entry]) => new Date(entry.startedAt).getTime() <= untilTime
915
+ trajectories = trajectories.filter(
916
+ (trajectory) => new Date(trajectory.startedAt).getTime() <= untilTime
958
917
  );
959
918
  }
960
919
  const sortBy = query.sortBy ?? "startedAt";
961
920
  const sortOrder = query.sortOrder ?? "desc";
962
- entries.sort((a, b) => {
963
- const aVal = a[1][sortBy] ?? "";
964
- const bVal = b[1][sortBy] ?? "";
921
+ trajectories.sort((a, b) => {
922
+ const aVal = this.getSortValue(a, sortBy);
923
+ const bVal = this.getSortValue(b, sortBy);
965
924
  const cmp = String(aVal).localeCompare(String(bVal));
966
925
  return sortOrder === "asc" ? cmp : -cmp;
967
926
  });
968
927
  const offset = query.offset ?? 0;
969
928
  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
- );
929
+ trajectories = trajectories.slice(offset, offset + limit);
930
+ return trajectories.map((trajectory) => this.toSummary(trajectory));
989
931
  }
990
932
  /**
991
933
  * Delete a trajectory
992
934
  */
993
935
  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
- });
936
+ await this.deleteWithSummary(id);
1011
937
  }
1012
938
  /**
1013
939
  * Search trajectories by text
@@ -1040,12 +966,395 @@ var FileStorage = class {
1040
966
  }
1041
967
  return matches;
1042
968
  }
969
+ /**
970
+ * Mark a trajectory as compacted without writing to a shared index.
971
+ */
972
+ async markCompacted(id, compactedInto) {
973
+ const markedIds = await this.markCompactedMany([id], compactedInto);
974
+ return markedIds.has(id);
975
+ }
976
+ /**
977
+ * Mark multiple trajectories as compacted with one filesystem scan.
978
+ */
979
+ async markCompactedMany(ids, compactedInto) {
980
+ const pathsById = await this.findTrajectoryFilePathsForIds(ids);
981
+ const markedIds = /* @__PURE__ */ new Set();
982
+ const compactedAt = (/* @__PURE__ */ new Date()).toISOString();
983
+ const writes = [];
984
+ for (const [id, paths] of pathsById.entries()) {
985
+ if (paths.length === 0) {
986
+ continue;
987
+ }
988
+ markedIds.add(id);
989
+ const marker = {
990
+ trajectoryId: id,
991
+ compactedInto,
992
+ compactedAt
993
+ };
994
+ for (const filePath of paths) {
995
+ writes.push(
996
+ writeFile(
997
+ this.getCompactionMarkerPath(filePath, id),
998
+ JSON.stringify(marker, null, 2),
999
+ "utf-8"
1000
+ )
1001
+ );
1002
+ }
1003
+ }
1004
+ await Promise.all(writes);
1005
+ return markedIds;
1006
+ }
1007
+ /**
1008
+ * Return trajectory IDs that have a per-trajectory compaction marker.
1009
+ */
1010
+ async getCompactedTrajectoryIds() {
1011
+ const markerPaths = await this.listCompactionMarkerFiles();
1012
+ const compactedIds = /* @__PURE__ */ new Set();
1013
+ for (const markerPath of markerPaths) {
1014
+ try {
1015
+ const marker = JSON.parse(
1016
+ await readFile(markerPath, "utf-8")
1017
+ );
1018
+ const trajectoryId = typeof marker.trajectoryId === "string" ? marker.trajectoryId : this.getTrajectoryIdFromCompactionMarkerPath(markerPath);
1019
+ if (trajectoryId && typeof marker.compactedInto === "string") {
1020
+ compactedIds.add(trajectoryId);
1021
+ }
1022
+ } catch {
1023
+ }
1024
+ }
1025
+ return compactedIds;
1026
+ }
1027
+ /**
1028
+ * Delete a trajectory and return file counts for CLI reporting.
1029
+ */
1030
+ async deleteWithSummary(id) {
1031
+ return this.deleteManyWithSummary([id]);
1032
+ }
1033
+ /**
1034
+ * Delete multiple trajectories with one filesystem scan.
1035
+ */
1036
+ async deleteManyWithSummary(ids) {
1037
+ const summary = {
1038
+ removedTrajectories: 0,
1039
+ deletedJsonFiles: 0,
1040
+ deletedMarkdownFiles: 0,
1041
+ deletedTraceFiles: 0,
1042
+ deletedCompactionFiles: 0
1043
+ };
1044
+ const pathsById = await this.findTrajectoryFilePathsForIds(ids);
1045
+ const deletedPaths = /* @__PURE__ */ new Set();
1046
+ for (const paths of pathsById.values()) {
1047
+ for (const filePath of paths) {
1048
+ if (deletedPaths.has(filePath)) {
1049
+ continue;
1050
+ }
1051
+ deletedPaths.add(filePath);
1052
+ await this.removeTrajectoryFile(filePath, summary);
1053
+ }
1054
+ }
1055
+ return summary;
1056
+ }
1043
1057
  /**
1044
1058
  * Close storage (no-op for file storage)
1045
1059
  */
1046
1060
  async close() {
1047
1061
  }
1048
1062
  // Private helpers
1063
+ getActiveCandidatePaths(id) {
1064
+ if (!isSafeTrajectoryId(id)) {
1065
+ return [];
1066
+ }
1067
+ return [
1068
+ join(this.activeDir, id, TRAJECTORY_FILE),
1069
+ // Legacy layout from v0.5.x and earlier.
1070
+ join(this.activeDir, `${id}.json`)
1071
+ ];
1072
+ }
1073
+ async loadAllTrajectories() {
1074
+ const files = await this.listTrajectoryFiles();
1075
+ const trajectories = /* @__PURE__ */ new Map();
1076
+ for (const filePath of files) {
1077
+ const trajectory = await this.readTrajectoryOrNull(filePath);
1078
+ if (!trajectory) {
1079
+ continue;
1080
+ }
1081
+ const current = trajectories.get(trajectory.id);
1082
+ if (!current || this.isNewerTrajectory(trajectory, current)) {
1083
+ trajectories.set(trajectory.id, trajectory);
1084
+ }
1085
+ }
1086
+ return Array.from(trajectories.values());
1087
+ }
1088
+ async listTrajectoryFiles() {
1089
+ const [activeFiles, completedFiles] = await Promise.all([
1090
+ this.collectTrajectoryFiles(this.activeDir),
1091
+ this.collectTrajectoryFiles(this.completedDir)
1092
+ ]);
1093
+ return [...activeFiles, ...completedFiles];
1094
+ }
1095
+ async collectTrajectoryFiles(dir) {
1096
+ const files = [];
1097
+ await this.walkJsonFilesInto(dir, files);
1098
+ return files;
1099
+ }
1100
+ async findTrajectoryFilePaths(id) {
1101
+ const pathsById = await this.findTrajectoryFilePathsForIds([id]);
1102
+ return pathsById.get(id) ?? [];
1103
+ }
1104
+ async findTrajectoryFilePathsForIds(ids) {
1105
+ const targetIds = new Set(Array.from(ids).filter(isSafeTrajectoryId));
1106
+ const pathsById = new Map(
1107
+ Array.from(targetIds).map((id) => [id, []])
1108
+ );
1109
+ if (targetIds.size === 0) {
1110
+ return pathsById;
1111
+ }
1112
+ const allFiles = await this.listTrajectoryFiles();
1113
+ for (const filePath of allFiles) {
1114
+ const trajectoryId = this.getTrajectoryIdFromPath(filePath);
1115
+ if (!trajectoryId || !targetIds.has(trajectoryId)) {
1116
+ continue;
1117
+ }
1118
+ pathsById.get(trajectoryId)?.push(filePath);
1119
+ }
1120
+ return pathsById;
1121
+ }
1122
+ getTrajectoryIdFromPath(filePath) {
1123
+ if (basename(filePath) === TRAJECTORY_FILE) {
1124
+ const id = basename(dirname(filePath));
1125
+ return isSafeTrajectoryId(id) ? id : void 0;
1126
+ }
1127
+ const name = basename(filePath);
1128
+ if (name.endsWith(".json")) {
1129
+ const id = name.slice(0, -".json".length);
1130
+ return isSafeTrajectoryId(id) ? id : void 0;
1131
+ }
1132
+ return void 0;
1133
+ }
1134
+ async removeTrajectoryFiles(paths, exceptPath) {
1135
+ const summary = this.emptyDeleteSummary();
1136
+ for (const filePath of paths) {
1137
+ if (filePath === exceptPath) {
1138
+ continue;
1139
+ }
1140
+ await this.removeTrajectoryFile(filePath, summary);
1141
+ }
1142
+ }
1143
+ async removeTrajectoryFile(filePath, summary) {
1144
+ if (basename(filePath) === TRAJECTORY_FILE) {
1145
+ const trajectoryDir = dirname(filePath);
1146
+ await this.countDirectoryTrajectoryFiles(trajectoryDir, summary);
1147
+ await this.removeFileIfExists(
1148
+ join(dirname(trajectoryDir), `${basename(trajectoryDir)}.trace.json`),
1149
+ "trace",
1150
+ summary
1151
+ );
1152
+ await rm(trajectoryDir, { recursive: true, force: true });
1153
+ return;
1154
+ }
1155
+ await this.removeFileIfExists(filePath, "json", summary);
1156
+ await this.removeFileIfExists(
1157
+ getMarkdownOutputPath(filePath),
1158
+ "markdown",
1159
+ summary
1160
+ );
1161
+ await this.removeFileIfExists(
1162
+ getTraceOutputPath(filePath),
1163
+ "trace",
1164
+ summary
1165
+ );
1166
+ await this.removeFileIfExists(
1167
+ getLegacyCompactionMarkerPath(filePath),
1168
+ "compaction",
1169
+ summary
1170
+ );
1171
+ }
1172
+ async countDirectoryTrajectoryFiles(trajectoryDir, summary) {
1173
+ await this.countFileIfExists(
1174
+ join(trajectoryDir, TRAJECTORY_FILE),
1175
+ "json",
1176
+ summary
1177
+ );
1178
+ await this.countFileIfExists(
1179
+ join(trajectoryDir, SUMMARY_FILE),
1180
+ "markdown",
1181
+ summary
1182
+ );
1183
+ await this.countFileIfExists(
1184
+ join(trajectoryDir, `${basename(trajectoryDir)}.trace.json`),
1185
+ "trace",
1186
+ summary
1187
+ );
1188
+ await this.countFileIfExists(
1189
+ join(trajectoryDir, "trace.json"),
1190
+ "trace",
1191
+ summary
1192
+ );
1193
+ await this.countFileIfExists(
1194
+ join(trajectoryDir, COMPACTION_FILE),
1195
+ "compaction",
1196
+ summary
1197
+ );
1198
+ }
1199
+ async removeFileIfExists(path2, kind, summary) {
1200
+ if (!existsSync(path2)) {
1201
+ return;
1202
+ }
1203
+ await rm(path2, { force: true });
1204
+ this.incrementDeleteSummary(kind, summary);
1205
+ }
1206
+ async countFileIfExists(path2, kind, summary) {
1207
+ if (existsSync(path2)) {
1208
+ this.incrementDeleteSummary(kind, summary);
1209
+ }
1210
+ }
1211
+ incrementDeleteSummary(kind, summary) {
1212
+ if (kind === "json") {
1213
+ summary.deletedJsonFiles += 1;
1214
+ summary.removedTrajectories += 1;
1215
+ } else if (kind === "markdown") {
1216
+ summary.deletedMarkdownFiles += 1;
1217
+ } else if (kind === "trace") {
1218
+ summary.deletedTraceFiles += 1;
1219
+ } else {
1220
+ summary.deletedCompactionFiles += 1;
1221
+ }
1222
+ }
1223
+ emptyDeleteSummary() {
1224
+ return {
1225
+ removedTrajectories: 0,
1226
+ deletedJsonFiles: 0,
1227
+ deletedMarkdownFiles: 0,
1228
+ deletedTraceFiles: 0,
1229
+ deletedCompactionFiles: 0
1230
+ };
1231
+ }
1232
+ async listCompactionMarkerFiles() {
1233
+ const markerPaths = [];
1234
+ await this.walkFilesInto(
1235
+ this.activeDir,
1236
+ markerPaths,
1237
+ isCompactionMarkerFile
1238
+ );
1239
+ await this.walkFilesInto(
1240
+ this.completedDir,
1241
+ markerPaths,
1242
+ isCompactionMarkerFile
1243
+ );
1244
+ return markerPaths;
1245
+ }
1246
+ getCompactionMarkerPath(filePath, id) {
1247
+ if (basename(filePath) === TRAJECTORY_FILE) {
1248
+ return join(dirname(filePath), COMPACTION_FILE);
1249
+ }
1250
+ return join(dirname(filePath), `${id}${LEGACY_COMPACTION_SUFFIX}`);
1251
+ }
1252
+ getTrajectoryIdFromCompactionMarkerPath(markerPath) {
1253
+ if (basename(markerPath) === COMPACTION_FILE) {
1254
+ const id = basename(dirname(markerPath));
1255
+ return id.startsWith("traj_") ? id : void 0;
1256
+ }
1257
+ const markerName = basename(markerPath);
1258
+ return markerName.endsWith(LEGACY_COMPACTION_SUFFIX) ? markerName.slice(0, -LEGACY_COMPACTION_SUFFIX.length) : void 0;
1259
+ }
1260
+ async migrateLegacyIndexCompactionMarkers() {
1261
+ const indexPath = join(this.trajectoriesDir, "index.json");
1262
+ if (!existsSync(indexPath)) {
1263
+ return;
1264
+ }
1265
+ let parsed;
1266
+ try {
1267
+ parsed = JSON.parse(await readFile(indexPath, "utf-8"));
1268
+ } catch {
1269
+ return;
1270
+ }
1271
+ if (parsed === null || typeof parsed !== "object") {
1272
+ return;
1273
+ }
1274
+ const trajectories = parsed.trajectories;
1275
+ if (trajectories === null || typeof trajectories !== "object" || Array.isArray(trajectories)) {
1276
+ return;
1277
+ }
1278
+ await Promise.all(
1279
+ Object.entries(trajectories).map(async ([id, entry]) => {
1280
+ if (entry === null || typeof entry !== "object" || !isSafeTrajectoryId(id)) {
1281
+ return;
1282
+ }
1283
+ const compactedInto = entry.compactedInto;
1284
+ const path2 = entry.path;
1285
+ if (typeof compactedInto !== "string") {
1286
+ return;
1287
+ }
1288
+ const paths = typeof path2 === "string" && existsSync(path2) && this.isPathInsideTrajectoriesDir(path2) ? [path2] : await this.findTrajectoryFilePaths(id);
1289
+ if (paths.length === 0) return;
1290
+ const marker = {
1291
+ trajectoryId: id,
1292
+ compactedInto,
1293
+ compactedAt: (/* @__PURE__ */ new Date()).toISOString()
1294
+ };
1295
+ await Promise.all(
1296
+ paths.map(
1297
+ (filePath) => writeFile(
1298
+ this.getCompactionMarkerPath(filePath, id),
1299
+ JSON.stringify(marker, null, 2),
1300
+ "utf-8"
1301
+ )
1302
+ )
1303
+ );
1304
+ })
1305
+ );
1306
+ }
1307
+ isPathInsideTrajectoriesDir(path2) {
1308
+ const rel = relative(resolve(this.trajectoriesDir), resolve(path2));
1309
+ return Boolean(rel && !rel.startsWith("..") && !isAbsolute(rel));
1310
+ }
1311
+ async walkFilesInto(dir, out, predicate) {
1312
+ let entries;
1313
+ try {
1314
+ entries = await readdir(dir, { withFileTypes: true });
1315
+ } catch (error) {
1316
+ if (error.code === "ENOENT") return;
1317
+ throw error;
1318
+ }
1319
+ for (const entry of entries) {
1320
+ const entryPath = join(dir, entry.name);
1321
+ if (entry.isDirectory()) {
1322
+ await this.walkFilesInto(entryPath, out, predicate);
1323
+ } else if (entry.isFile() && predicate(entry.name)) {
1324
+ out.push(entryPath);
1325
+ }
1326
+ }
1327
+ }
1328
+ getSortValue(trajectory, sortBy) {
1329
+ if (sortBy === "title") {
1330
+ return trajectory.task.title;
1331
+ }
1332
+ return trajectory[sortBy] ?? "";
1333
+ }
1334
+ toSummary(trajectory) {
1335
+ return {
1336
+ id: trajectory.id,
1337
+ title: trajectory.task.title,
1338
+ status: trajectory.status,
1339
+ startedAt: trajectory.startedAt,
1340
+ completedAt: trajectory.completedAt,
1341
+ confidence: trajectory.retrospective?.confidence,
1342
+ chapterCount: trajectory.chapters.length,
1343
+ decisionCount: trajectory.chapters.reduce(
1344
+ (count, chapter) => count + chapter.events.filter((event) => event.type === "decision").length,
1345
+ 0
1346
+ )
1347
+ };
1348
+ }
1349
+ isNewerTrajectory(candidate, current) {
1350
+ const candidateTime = new Date(
1351
+ candidate.completedAt ?? candidate.startedAt
1352
+ ).getTime();
1353
+ const currentTime = new Date(
1354
+ current.completedAt ?? current.startedAt
1355
+ ).getTime();
1356
+ return candidateTime > currentTime;
1357
+ }
1049
1358
  /**
1050
1359
  * Read a trajectory file and return a tagged result so callers can
1051
1360
  * distinguish missing files, malformed JSON, and schema violations.
@@ -1085,82 +1394,25 @@ var FileStorage = class {
1085
1394
  const result = await this.readTrajectoryFile(path2);
1086
1395
  return result.ok ? result.trajectory : null;
1087
1396
  }
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
1397
  };
1398
+ function isTrajectoryJsonFile(name) {
1399
+ return name === TRAJECTORY_FILE || name.endsWith(".json") && name !== "index.json" && !name.endsWith(".trace.json") && !name.endsWith(LEGACY_COMPACTION_SUFFIX) && name !== COMPACTION_FILE;
1400
+ }
1401
+ function isSafeTrajectoryId(id) {
1402
+ return id.length > 0 && !id.includes("..") && !id.includes("/") && !id.includes("\\");
1403
+ }
1404
+ function isCompactionMarkerFile(name) {
1405
+ return name === COMPACTION_FILE || name.endsWith(LEGACY_COMPACTION_SUFFIX);
1406
+ }
1407
+ function getMarkdownOutputPath(outputPath) {
1408
+ return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".md") : `${outputPath}.md`;
1409
+ }
1410
+ function getTraceOutputPath(outputPath) {
1411
+ return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".trace.json") : `${outputPath}.trace.json`;
1412
+ }
1413
+ function getLegacyCompactionMarkerPath(outputPath) {
1414
+ return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(LEGACY_COMPACTION_SUFFIX) : `${outputPath}${LEGACY_COMPACTION_SUFFIX}`;
1415
+ }
1164
1416
 
1165
1417
  // src/cli/commands/abandon.ts
1166
1418
  function registerAbandonCommand(program2) {
@@ -1183,13 +1435,7 @@ function registerAbandonCommand(program2) {
1183
1435
 
1184
1436
  // src/cli/commands/compact.ts
1185
1437
  import { execFileSync } from "child_process";
1186
- import {
1187
- existsSync as existsSync3,
1188
- mkdirSync,
1189
- readFileSync as readFileSync2,
1190
- unlinkSync,
1191
- writeFileSync
1192
- } from "fs";
1438
+ import { existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
1193
1439
  import { dirname as dirname2, join as join4 } from "path";
1194
1440
 
1195
1441
  // src/compact/config.ts
@@ -1247,7 +1493,7 @@ function loadFileConfig() {
1247
1493
  }
1248
1494
  function getPrimaryConfigDir() {
1249
1495
  const searchPaths = getSearchPaths();
1250
- return searchPaths[0] ?? join2(process.cwd(), ".trajectories");
1496
+ return searchPaths[0] ?? getDefaultTrajectoryDataDir();
1251
1497
  }
1252
1498
  function readStringEnv(name) {
1253
1499
  return readString(process.env[name]);
@@ -1894,7 +2140,7 @@ function buildCliArgs(cli) {
1894
2140
  }
1895
2141
  }
1896
2142
  function spawnWithStdin(command, args, input) {
1897
- return new Promise((resolve, reject) => {
2143
+ return new Promise((resolve2, reject) => {
1898
2144
  const child = spawn(command, args, {
1899
2145
  timeout: 3e5,
1900
2146
  stdio: ["pipe", "pipe", "pipe"]
@@ -1912,7 +2158,7 @@ function spawnWithStdin(command, args, input) {
1912
2158
  new Error(`CLI exited with code ${code}: ${stderr.slice(0, 200)}`)
1913
2159
  );
1914
2160
  } else {
1915
- resolve(Buffer.concat(chunks).toString().trim());
2161
+ resolve2(Buffer.concat(chunks).toString().trim());
1916
2162
  }
1917
2163
  });
1918
2164
  child.stdin.write(input);
@@ -2337,7 +2583,7 @@ function registerCompactCommand(program2) {
2337
2583
  "Comma-separated focus areas to emphasize in LLM compaction"
2338
2584
  ).option("--markdown", "Also write a Markdown companion file").option("--no-markdown", "Skip writing a Markdown companion file").option(
2339
2585
  "--discard-sources",
2340
- "After saving the compaction, delete source trajectory JSON/MD/trace files and remove their index entries"
2586
+ "After saving the compaction, delete source trajectory JSON/MD/trace files"
2341
2587
  ).option("--dry-run", "Preview what would be compacted without saving").option("--output <path>", "Output path for compacted trajectory").action(async (options) => {
2342
2588
  const trajectories = await loadTrajectories(options);
2343
2589
  if (trajectories.length === 0) {
@@ -2378,7 +2624,7 @@ function registerCompactCommand(program2) {
2378
2624
  markdownEnabled
2379
2625
  );
2380
2626
  if (options.discardSources) {
2381
- const discardSummary = discardSourceTrajectories(trajectories);
2627
+ const discardSummary = await discardSourceTrajectories(trajectories);
2382
2628
  printDiscardSummary(discardSummary);
2383
2629
  } else {
2384
2630
  await markTrajectoriesAsCompacted(
@@ -2390,7 +2636,7 @@ function registerCompactCommand(program2) {
2390
2636
  Compacted trajectory saved to: ${outputPath2}`);
2391
2637
  if (markdownEnabled) {
2392
2638
  console.log(
2393
- `Markdown summary saved to: ${getMarkdownOutputPath(outputPath2)}`
2639
+ `Markdown summary saved to: ${getMarkdownOutputPath2(outputPath2)}`
2394
2640
  );
2395
2641
  }
2396
2642
  printCompactedSummary(mechanicalCompacted);
@@ -2439,7 +2685,7 @@ Compacted trajectory saved to: ${outputPath2}`);
2439
2685
  const outputPath = options.output || getDefaultOutputPath(compacted, options.workflow);
2440
2686
  saveCompactionArtifacts(compacted, outputPath, markdownEnabled);
2441
2687
  if (options.discardSources) {
2442
- const discardSummary = discardSourceTrajectories(trajectories);
2688
+ const discardSummary = await discardSourceTrajectories(trajectories);
2443
2689
  printDiscardSummary(discardSummary);
2444
2690
  } else {
2445
2691
  await markTrajectoriesAsCompacted(trajectories, compacted.id);
@@ -2448,7 +2694,7 @@ Compacted trajectory saved to: ${outputPath2}`);
2448
2694
  Compacted trajectory saved to: ${outputPath}`);
2449
2695
  if (markdownEnabled) {
2450
2696
  console.log(
2451
- `Markdown summary saved to: ${getMarkdownOutputPath(outputPath)}`
2697
+ `Markdown summary saved to: ${getMarkdownOutputPath2(outputPath)}`
2452
2698
  );
2453
2699
  }
2454
2700
  printCompactedSummary(compacted);
@@ -2467,19 +2713,12 @@ async function loadTrajectories(options) {
2467
2713
  return [trimmed, trimmed.slice(0, 7)];
2468
2714
  })
2469
2715
  ) : null;
2470
- const compactedIds = options.all ? /* @__PURE__ */ new Set() : getCompactedTrajectoryIds();
2716
+ const compactedIds = options.all ? /* @__PURE__ */ new Set() : await getCompactedTrajectoryIds();
2471
2717
  const searchPaths = getSearchPaths();
2472
2718
  const seenIds = /* @__PURE__ */ new Set();
2473
2719
  for (const searchPath of searchPaths) {
2474
2720
  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
- }
2721
+ const storage = createStorageForSearchPath(searchPath);
2483
2722
  await storage.initialize();
2484
2723
  const summaries = await storage.list({
2485
2724
  status: "completed",
@@ -2525,6 +2764,19 @@ async function loadTrajectories(options) {
2525
2764
  }
2526
2765
  return trajectories;
2527
2766
  }
2767
+ function createStorageForSearchPath(searchPath) {
2768
+ const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
2769
+ process.env.TRAJECTORIES_DATA_DIR = searchPath;
2770
+ try {
2771
+ return new FileStorage();
2772
+ } finally {
2773
+ if (originalDataDir !== void 0) {
2774
+ process.env.TRAJECTORIES_DATA_DIR = originalDataDir;
2775
+ } else {
2776
+ delete process.env.TRAJECTORIES_DATA_DIR;
2777
+ }
2778
+ }
2779
+ }
2528
2780
  function getBranchCommits(targetBranch) {
2529
2781
  const commits = /* @__PURE__ */ new Set();
2530
2782
  try {
@@ -2549,21 +2801,17 @@ function getBranchCommits(targetBranch) {
2549
2801
  }
2550
2802
  return commits;
2551
2803
  }
2552
- function getCompactedTrajectoryIds() {
2804
+ async function getCompactedTrajectoryIds() {
2553
2805
  const compacted = /* @__PURE__ */ new Set();
2554
2806
  const searchPaths = getSearchPaths();
2555
2807
  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 {
2808
+ if (!existsSync3(searchPath)) {
2809
+ continue;
2810
+ }
2811
+ const storage = createStorageForSearchPath(searchPath);
2812
+ await storage.initialize();
2813
+ for (const id of await storage.getCompactedTrajectoryIds()) {
2814
+ compacted.add(id);
2567
2815
  }
2568
2816
  }
2569
2817
  return compacted;
@@ -2571,92 +2819,45 @@ function getCompactedTrajectoryIds() {
2571
2819
  async function markTrajectoriesAsCompacted(trajectories, compactedIntoId) {
2572
2820
  const searchPaths = getSearchPaths();
2573
2821
  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 {
2822
+ if (!existsSync3(searchPath)) {
2823
+ continue;
2591
2824
  }
2825
+ const storage = createStorageForSearchPath(searchPath);
2826
+ await storage.initialize();
2827
+ await storage.markCompactedMany(
2828
+ trajectories.map((trajectory) => trajectory.id),
2829
+ compactedIntoId
2830
+ );
2592
2831
  }
2593
2832
  }
2594
- function discardSourceTrajectories(trajectories) {
2595
- const sourceIds = new Set(trajectories.map((trajectory) => trajectory.id));
2833
+ async function discardSourceTrajectories(trajectories) {
2596
2834
  const summary = {
2597
- removedIndexEntries: 0,
2835
+ removedTrajectories: 0,
2598
2836
  deletedJsonFiles: 0,
2599
2837
  deletedMarkdownFiles: 0,
2600
- deletedTraceFiles: 0
2838
+ deletedTraceFiles: 0,
2839
+ deletedCompactionFiles: 0
2601
2840
  };
2602
2841
  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 {
2842
+ if (!existsSync3(searchPath)) {
2614
2843
  continue;
2615
2844
  }
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
- }
2845
+ const storage = createStorageForSearchPath(searchPath);
2846
+ await storage.initialize();
2847
+ const deleteSummary = await storage.deleteManyWithSummary(
2848
+ trajectories.map((trajectory) => trajectory.id)
2849
+ );
2850
+ summary.removedTrajectories += deleteSummary.removedTrajectories;
2851
+ summary.deletedJsonFiles += deleteSummary.deletedJsonFiles;
2852
+ summary.deletedMarkdownFiles += deleteSummary.deletedMarkdownFiles;
2853
+ summary.deletedTraceFiles += deleteSummary.deletedTraceFiles;
2854
+ summary.deletedCompactionFiles += deleteSummary.deletedCompactionFiles;
2637
2855
  }
2638
2856
  return summary;
2639
2857
  }
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
2858
  function printDiscardSummary(summary) {
2658
2859
  console.log(
2659
- `Discarded source trajectories: ${summary.removedIndexEntries} index entries, ${summary.deletedJsonFiles} JSON files, ${summary.deletedMarkdownFiles} Markdown files, ${summary.deletedTraceFiles} trace files`
2860
+ `Discarded source trajectories: ${summary.removedTrajectories} trajectories, ${summary.deletedJsonFiles} JSON files, ${summary.deletedMarkdownFiles} Markdown files, ${summary.deletedTraceFiles} trace files, ${summary.deletedCompactionFiles} compaction markers`
2660
2861
  );
2661
2862
  }
2662
2863
  function parseRelativeDate(input) {
@@ -2882,7 +3083,7 @@ function getProviderLabel(provider) {
2882
3083
  return "LLM";
2883
3084
  }
2884
3085
  function getDefaultOutputPath(compacted, workflowId) {
2885
- const trajDir = process.env.TRAJECTORIES_DATA_DIR || ".trajectories";
3086
+ const trajDir = process.env.TRAJECTORIES_DATA_DIR || getDefaultTrajectoryDataDir();
2886
3087
  const compactedDir = join4(trajDir, "compacted");
2887
3088
  if (!existsSync3(compactedDir)) {
2888
3089
  mkdirSync(compactedDir, { recursive: true });
@@ -2901,12 +3102,12 @@ function saveCompactionArtifacts(compacted, outputPath, markdownEnabled) {
2901
3102
  writeFileSync(outputPath, JSON.stringify(compacted, null, 2));
2902
3103
  if (markdownEnabled) {
2903
3104
  writeFileSync(
2904
- getMarkdownOutputPath(outputPath),
3105
+ getMarkdownOutputPath2(outputPath),
2905
3106
  renderCompactionMarkdown(compacted)
2906
3107
  );
2907
3108
  }
2908
3109
  }
2909
- function getMarkdownOutputPath(outputPath) {
3110
+ function getMarkdownOutputPath2(outputPath) {
2910
3111
  return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".md") : `${outputPath}.md`;
2911
3112
  }
2912
3113
  function renderCompactionMarkdown(compacted) {
@@ -3229,7 +3430,7 @@ function createTraceRef(startRef, traceId) {
3229
3430
 
3230
3431
  // src/core/trailers.ts
3231
3432
  import { execSync as execSync2 } from "child_process";
3232
- import { readFileSync as readFileSync3 } from "fs";
3433
+ import { readFileSync as readFileSync2 } from "fs";
3233
3434
  function getCommitsBetween(startRef, endRef = "HEAD") {
3234
3435
  if (!isGitRepo()) {
3235
3436
  return [];
@@ -3298,7 +3499,7 @@ if [ "$COMMIT_SOURCE" = "merge" ] || [ "$COMMIT_SOURCE" = "squash" ] || [ "$COMM
3298
3499
  fi
3299
3500
 
3300
3501
  # Find the trajectories data directory
3301
- TRAJ_DIR="\${TRAJECTORIES_DATA_DIR:-$(git rev-parse --show-toplevel)/.trajectories}"
3502
+ TRAJ_DIR="\${TRAJECTORIES_DATA_DIR:-$(git rev-parse --show-toplevel)/.agentworkforce/trajectories}"
3302
3503
  ACTIVE_DIR="$TRAJ_DIR/active"
3303
3504
 
3304
3505
  # Check if there's an active trajectory
@@ -3342,7 +3543,7 @@ function detectExistingHook() {
3342
3543
  }).trim();
3343
3544
  const hookPath = `${hooksDir}/hooks/prepare-commit-msg`;
3344
3545
  try {
3345
- const content = readFileSync3(hookPath, "utf-8");
3546
+ const content = readFileSync2(hookPath, "utf-8");
3346
3547
  if (content.includes("agent-trajectories")) {
3347
3548
  return "ours";
3348
3549
  }
@@ -3358,7 +3559,7 @@ function detectExistingHook() {
3358
3559
  // src/cli/commands/complete.ts
3359
3560
  async function saveTraceFile(trajectory, trace) {
3360
3561
  const dataDir = process.env.TRAJECTORIES_DATA_DIR;
3361
- const baseDir = dataDir ? dataDir : join5(process.cwd(), ".trajectories");
3562
+ const baseDir = dataDir ? dataDir : getDefaultTrajectoryDataDir();
3362
3563
  const completedDir = join5(baseDir, "completed");
3363
3564
  const date = new Date(trajectory.completedAt ?? trajectory.startedAt);
3364
3565
  const monthDir = join5(
@@ -3486,7 +3687,7 @@ function registerDoctorCommand(program2) {
3486
3687
  "List trajectory files that fail to load; optionally quarantine them"
3487
3688
  ).option(
3488
3689
  "--quarantine",
3489
- "Move invalid files to .trajectories/invalid/ so reconcile stops scanning them"
3690
+ "Move invalid files to .agentworkforce/trajectories/invalid/ so reconcile stops scanning them"
3490
3691
  ).action(async (opts) => {
3491
3692
  const storage = new FileStorage();
3492
3693
  await storage.initialize();
@@ -3504,7 +3705,7 @@ function registerDoctorCommand(program2) {
3504
3705
  }
3505
3706
  if (!opts.quarantine) {
3506
3707
  console.log(
3507
- "\nRun `trail doctor --quarantine` to move these files into .trajectories/invalid/."
3708
+ "\nRun `trail doctor --quarantine` to move these files into .agentworkforce/trajectories/invalid/."
3508
3709
  );
3509
3710
  return;
3510
3711
  }
@@ -3602,8 +3803,8 @@ function registerEnableCommand(program2) {
3602
3803
  console.error("Remove it manually if needed");
3603
3804
  throw new Error("Hook not ours");
3604
3805
  }
3605
- const { unlink: unlink2 } = await import("fs/promises");
3606
- await unlink2(hookPath);
3806
+ const { unlink } = await import("fs/promises");
3807
+ await unlink(hookPath);
3607
3808
  console.log("Trajectory hook removed");
3608
3809
  });
3609
3810
  }
@@ -4308,7 +4509,10 @@ function registerExportCommand(program2) {
4308
4509
  openInBrowser(options.output);
4309
4510
  }
4310
4511
  } else if (options.open && options.format === "html") {
4311
- const outputDir = join7(process.cwd(), ".trajectories", "html");
4512
+ const outputDir = join7(
4513
+ process.env.TRAJECTORIES_DATA_DIR ?? getDefaultTrajectoryDataDir(),
4514
+ "html"
4515
+ );
4312
4516
  await mkdir4(outputDir, { recursive: true });
4313
4517
  const filePath = join7(outputDir, `${trajectory.id}.html`);
4314
4518
  await writeFile4(filePath, output, "utf-8");