agent-trajectories 0.5.7 → 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/README.md +2 -2
- package/dist/{chunk-27AQPWHK.js → chunk-JMH3Z5BB.js} +579 -262
- package/dist/chunk-JMH3Z5BB.js.map +1 -0
- package/dist/cli/index.js +707 -372
- package/dist/cli/index.js.map +1 -1
- package/dist/{index-C7XhwsoN.d.ts → index-B4yIThRL.d.ts} +111 -39
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-27AQPWHK.js.map +0 -1
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
|
-
|
|
413
|
+
rm,
|
|
415
414
|
writeFile
|
|
416
415
|
} from "fs/promises";
|
|
417
|
-
import {
|
|
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));
|
|
@@ -594,22 +604,26 @@ function getSearchPaths() {
|
|
|
594
604
|
}
|
|
595
605
|
return [join(process.cwd(), ".trajectories")];
|
|
596
606
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
607
|
+
function describeReadFailure(reason, error) {
|
|
608
|
+
if (reason === "schema_violation" && error && typeof error === "object" && "issues" in error) {
|
|
609
|
+
const issues = error.issues ?? [];
|
|
610
|
+
if (issues.length > 0) {
|
|
611
|
+
const first = issues[0];
|
|
612
|
+
const where = first.path.length > 0 ? first.path.join(".") : "root";
|
|
613
|
+
const extra = issues.length > 1 ? ` (+${issues.length - 1} more)` : "";
|
|
614
|
+
return `${where}: ${first.message}${extra}`;
|
|
615
|
+
}
|
|
616
|
+
return "schema validation failed";
|
|
617
|
+
}
|
|
618
|
+
if (error instanceof Error) return error.message;
|
|
619
|
+
return String(error);
|
|
606
620
|
}
|
|
607
621
|
var FileStorage = class {
|
|
608
622
|
baseDir;
|
|
609
623
|
trajectoriesDir;
|
|
610
624
|
activeDir;
|
|
611
625
|
completedDir;
|
|
612
|
-
|
|
626
|
+
lastReconcileSummary;
|
|
613
627
|
constructor(baseDir) {
|
|
614
628
|
this.baseDir = baseDir ?? process.cwd();
|
|
615
629
|
const dataDir = process.env.TRAJECTORIES_DATA_DIR;
|
|
@@ -620,7 +634,6 @@ var FileStorage = class {
|
|
|
620
634
|
}
|
|
621
635
|
this.activeDir = join(this.trajectoriesDir, "active");
|
|
622
636
|
this.completedDir = join(this.trajectoriesDir, "completed");
|
|
623
|
-
this.indexPath = join(this.trajectoriesDir, "index.json");
|
|
624
637
|
}
|
|
625
638
|
/**
|
|
626
639
|
* Initialize storage directories
|
|
@@ -629,28 +642,22 @@ var FileStorage = class {
|
|
|
629
642
|
await mkdir(this.trajectoriesDir, { recursive: true });
|
|
630
643
|
await mkdir(this.activeDir, { recursive: true });
|
|
631
644
|
await mkdir(this.completedDir, { recursive: true });
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (!existsSync(this.indexPath)) {
|
|
635
|
-
await this.saveIndex(this.emptyIndex());
|
|
636
|
-
}
|
|
637
|
-
});
|
|
638
|
-
}
|
|
645
|
+
await this.migrateLegacyIndexCompactionMarkers();
|
|
646
|
+
await rm(join(this.trajectoriesDir, "index.json"), { force: true });
|
|
639
647
|
await this.reconcileIndex();
|
|
640
648
|
}
|
|
641
649
|
/**
|
|
642
|
-
* Scan active/ and completed/ recursively and
|
|
643
|
-
*
|
|
644
|
-
* 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.
|
|
645
652
|
*
|
|
646
653
|
* Handles three on-disk layouts in completed/:
|
|
647
654
|
* - flat: completed/{id}.json (legacy workforce data)
|
|
648
|
-
* - monthly: completed/YYYY-MM/{id}.json (
|
|
655
|
+
* - monthly: completed/YYYY-MM/{id}.json (legacy monthly layout)
|
|
656
|
+
* - directory: completed/YYYY-MM/{id}/trajectory.json (current layout)
|
|
649
657
|
* - nested: completed/.../{id}.json (defensive — any depth)
|
|
650
658
|
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
* added.
|
|
659
|
+
* The method name is kept for callers such as `trail doctor`, but no
|
|
660
|
+
* shared index file is written.
|
|
654
661
|
*/
|
|
655
662
|
async reconcileIndex() {
|
|
656
663
|
const summary = {
|
|
@@ -659,55 +666,32 @@ var FileStorage = class {
|
|
|
659
666
|
alreadyIndexed: 0,
|
|
660
667
|
skippedMalformedJson: 0,
|
|
661
668
|
skippedSchemaViolation: 0,
|
|
662
|
-
skippedIoError: 0
|
|
669
|
+
skippedIoError: 0,
|
|
670
|
+
failures: []
|
|
663
671
|
};
|
|
664
|
-
await
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
if (error.code !== "ENOENT") throw error;
|
|
676
|
-
}
|
|
677
|
-
await this.walkJsonFilesInto(this.completedDir, discovered);
|
|
678
|
-
for (const filePath of discovered) {
|
|
679
|
-
summary.scanned += 1;
|
|
680
|
-
const result = await this.readTrajectoryFile(filePath);
|
|
681
|
-
if (!result.ok) {
|
|
682
|
-
if (result.reason === "malformed_json") {
|
|
683
|
-
summary.skippedMalformedJson += 1;
|
|
684
|
-
} else if (result.reason === "schema_violation") {
|
|
685
|
-
summary.skippedSchemaViolation += 1;
|
|
686
|
-
} else {
|
|
687
|
-
summary.skippedIoError += 1;
|
|
688
|
-
}
|
|
689
|
-
continue;
|
|
690
|
-
}
|
|
691
|
-
const trajectory = result.trajectory;
|
|
692
|
-
if (index.trajectories[trajectory.id]) {
|
|
693
|
-
summary.alreadyIndexed += 1;
|
|
694
|
-
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;
|
|
695
683
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
};
|
|
703
|
-
summary.added += 1;
|
|
704
|
-
}
|
|
705
|
-
if (Object.keys(index.trajectories).length !== before) {
|
|
706
|
-
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;
|
|
707
690
|
}
|
|
708
|
-
|
|
691
|
+
summary.added += 1;
|
|
692
|
+
}
|
|
709
693
|
const hadSkips = summary.skippedMalformedJson + summary.skippedSchemaViolation + summary.skippedIoError > 0;
|
|
710
|
-
if (
|
|
694
|
+
if (hadSkips) {
|
|
711
695
|
const parts = [`reconciled ${summary.added}/${summary.scanned}`];
|
|
712
696
|
if (summary.skippedMalformedJson > 0) {
|
|
713
697
|
parts.push(`malformed: ${summary.skippedMalformedJson}`);
|
|
@@ -720,10 +704,76 @@ var FileStorage = class {
|
|
|
720
704
|
}
|
|
721
705
|
console.warn(`[trajectories] ${parts.join(", ")}`);
|
|
722
706
|
}
|
|
707
|
+
this.lastReconcileSummary = summary;
|
|
723
708
|
return summary;
|
|
724
709
|
}
|
|
725
710
|
/**
|
|
726
|
-
*
|
|
711
|
+
* Returns the most recent reconcile summary, if any. Lets the CLI
|
|
712
|
+
* inspect the failures collected during `initialize()` without having
|
|
713
|
+
* to re-walk the directory tree (and re-emit the warn line).
|
|
714
|
+
*/
|
|
715
|
+
getLastReconcileSummary() {
|
|
716
|
+
return this.lastReconcileSummary;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Move trajectory files that fail to load into `.trajectories/invalid/`
|
|
720
|
+
* so reconcile no longer scans them. Only quarantines parse and schema
|
|
721
|
+
* failures — transient io_error failures are left in place because the
|
|
722
|
+
* file may load fine on the next attempt.
|
|
723
|
+
*
|
|
724
|
+
* Returns the list of files that were moved (with their original paths
|
|
725
|
+
* and the destination directory) so the caller can report what changed.
|
|
726
|
+
*/
|
|
727
|
+
async quarantineInvalid() {
|
|
728
|
+
const summary = await this.reconcileIndex();
|
|
729
|
+
const targetDir = join(this.trajectoriesDir, "invalid");
|
|
730
|
+
const candidates = summary.failures.filter((f) => f.reason !== "io_error");
|
|
731
|
+
if (candidates.length === 0) {
|
|
732
|
+
return { moved: [], targetDir };
|
|
733
|
+
}
|
|
734
|
+
await mkdir(targetDir, { recursive: true });
|
|
735
|
+
const moved = [];
|
|
736
|
+
for (const failure of candidates) {
|
|
737
|
+
const dest = await this.resolveQuarantineDest(failure.path, targetDir);
|
|
738
|
+
try {
|
|
739
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
740
|
+
await rename(failure.path, dest);
|
|
741
|
+
moved.push(failure);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
console.warn(
|
|
744
|
+
`[trajectories] failed to quarantine ${failure.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return { moved, targetDir };
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Pick a destination path under `targetDir` for a quarantined file.
|
|
752
|
+
*
|
|
753
|
+
* Preserves the file's relative location under the trajectories root
|
|
754
|
+
* (e.g. `completed/2026-04/foo.json` → `invalid/completed/2026-04/foo.json`)
|
|
755
|
+
* so two invalid files that share a basename across `active/` and
|
|
756
|
+
* `completed/` don't collapse onto each other and silently overwrite.
|
|
757
|
+
*
|
|
758
|
+
* Falls back to a numeric-suffix scheme for paths that live outside
|
|
759
|
+
* the trajectories directory or that, after relative resolution, would
|
|
760
|
+
* still collide with something already quarantined.
|
|
761
|
+
*/
|
|
762
|
+
async resolveQuarantineDest(sourcePath, targetDir) {
|
|
763
|
+
const rel = relative(this.trajectoriesDir, sourcePath);
|
|
764
|
+
const safeRel = rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : basename(sourcePath);
|
|
765
|
+
let dest = join(targetDir, safeRel);
|
|
766
|
+
if (!existsSync(dest)) return dest;
|
|
767
|
+
const ext = safeRel.endsWith(".json") ? ".json" : "";
|
|
768
|
+
const stem = ext ? safeRel.slice(0, -ext.length) : safeRel;
|
|
769
|
+
for (let i = 1; i < 1e3; i += 1) {
|
|
770
|
+
dest = join(targetDir, `${stem}.${i}${ext}`);
|
|
771
|
+
if (!existsSync(dest)) return dest;
|
|
772
|
+
}
|
|
773
|
+
return dest;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Recursively collect trajectory JSON file paths under `dir` into `out`.
|
|
727
777
|
* Silently treats a missing directory as empty.
|
|
728
778
|
*/
|
|
729
779
|
async walkJsonFilesInto(dir, out) {
|
|
@@ -738,7 +788,7 @@ var FileStorage = class {
|
|
|
738
788
|
const entryPath = join(dir, entry.name);
|
|
739
789
|
if (entry.isDirectory()) {
|
|
740
790
|
await this.walkJsonFilesInto(entryPath, out);
|
|
741
|
-
} else if (entry.isFile() && entry.name
|
|
791
|
+
} else if (entry.isFile() && isTrajectoryJsonFile(entry.name)) {
|
|
742
792
|
out.push(entryPath);
|
|
743
793
|
}
|
|
744
794
|
}
|
|
@@ -762,56 +812,43 @@ var FileStorage = class {
|
|
|
762
812
|
}
|
|
763
813
|
const trajectory = validation.data;
|
|
764
814
|
const isCompleted = trajectory.status === "completed" || trajectory.status === "abandoned";
|
|
765
|
-
|
|
815
|
+
const existingPaths = await this.findTrajectoryFilePaths(trajectory.id);
|
|
816
|
+
let trajectoryDir;
|
|
766
817
|
if (isCompleted) {
|
|
767
818
|
const date = new Date(trajectory.completedAt ?? trajectory.startedAt);
|
|
768
819
|
const monthDir = join(
|
|
769
820
|
this.completedDir,
|
|
770
821
|
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`
|
|
771
822
|
);
|
|
772
|
-
|
|
773
|
-
filePath = join(monthDir, `${trajectory.id}.json`);
|
|
774
|
-
const activePath = join(this.activeDir, `${trajectory.id}.json`);
|
|
775
|
-
if (existsSync(activePath)) {
|
|
776
|
-
await unlink(activePath);
|
|
777
|
-
}
|
|
778
|
-
const mdPath = join(monthDir, `${trajectory.id}.md`);
|
|
779
|
-
const markdown = exportToMarkdown(trajectory);
|
|
780
|
-
await writeFile(mdPath, markdown, "utf-8");
|
|
823
|
+
trajectoryDir = join(monthDir, trajectory.id);
|
|
781
824
|
} else {
|
|
782
|
-
|
|
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");
|
|
783
833
|
}
|
|
784
834
|
await writeFile(filePath, JSON.stringify(trajectory, null, 2), "utf-8");
|
|
785
|
-
await this.updateIndex(trajectory, filePath);
|
|
786
835
|
}
|
|
787
836
|
/**
|
|
788
837
|
* Get a trajectory by ID
|
|
789
838
|
*/
|
|
790
839
|
async get(id) {
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const entry = index.trajectories[id];
|
|
797
|
-
if (entry?.path && existsSync(entry.path)) {
|
|
798
|
-
return this.readTrajectoryOrNull(entry.path);
|
|
799
|
-
}
|
|
800
|
-
try {
|
|
801
|
-
const flatPath = join(this.completedDir, `${id}.json`);
|
|
802
|
-
if (existsSync(flatPath)) {
|
|
803
|
-
return this.readTrajectoryOrNull(flatPath);
|
|
804
|
-
}
|
|
805
|
-
const months = await readdir(this.completedDir);
|
|
806
|
-
for (const month of months) {
|
|
807
|
-
const filePath = join(this.completedDir, month, `${id}.json`);
|
|
808
|
-
if (existsSync(filePath)) {
|
|
809
|
-
return this.readTrajectoryOrNull(filePath);
|
|
810
|
-
}
|
|
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;
|
|
811
845
|
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
|
|
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;
|
|
815
852
|
}
|
|
816
853
|
}
|
|
817
854
|
return null;
|
|
@@ -820,107 +857,61 @@ var FileStorage = class {
|
|
|
820
857
|
* Get the currently active trajectory
|
|
821
858
|
*/
|
|
822
859
|
async getActive() {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
826
|
-
if (jsonFiles.length === 0) {
|
|
827
|
-
return null;
|
|
828
|
-
}
|
|
829
|
-
let mostRecent = null;
|
|
830
|
-
let mostRecentTime = 0;
|
|
831
|
-
for (const file of jsonFiles) {
|
|
832
|
-
const trajectory = await this.readTrajectoryOrNull(
|
|
833
|
-
join(this.activeDir, file)
|
|
834
|
-
);
|
|
835
|
-
if (trajectory) {
|
|
836
|
-
const startTime = new Date(trajectory.startedAt).getTime();
|
|
837
|
-
if (startTime > mostRecentTime) {
|
|
838
|
-
mostRecentTime = startTime;
|
|
839
|
-
mostRecent = trajectory;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
return mostRecent;
|
|
844
|
-
} catch (error) {
|
|
845
|
-
if (error.code === "ENOENT") {
|
|
846
|
-
return null;
|
|
847
|
-
}
|
|
848
|
-
console.error("Error reading active trajectories:", error);
|
|
860
|
+
const activeFiles = await this.collectTrajectoryFiles(this.activeDir);
|
|
861
|
+
if (activeFiles.length === 0) {
|
|
849
862
|
return null;
|
|
850
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;
|
|
851
876
|
}
|
|
852
877
|
/**
|
|
853
878
|
* List trajectories with optional filtering
|
|
854
879
|
*/
|
|
855
880
|
async list(query) {
|
|
856
|
-
|
|
857
|
-
let entries = Object.entries(index.trajectories);
|
|
881
|
+
let trajectories = await this.loadAllTrajectories();
|
|
858
882
|
if (query.status) {
|
|
859
|
-
|
|
883
|
+
trajectories = trajectories.filter((t) => t.status === query.status);
|
|
860
884
|
}
|
|
861
885
|
if (query.since) {
|
|
862
886
|
const sinceTime = new Date(query.since).getTime();
|
|
863
|
-
|
|
864
|
-
(
|
|
887
|
+
trajectories = trajectories.filter(
|
|
888
|
+
(trajectory) => new Date(trajectory.startedAt).getTime() >= sinceTime
|
|
865
889
|
);
|
|
866
890
|
}
|
|
867
891
|
if (query.until) {
|
|
868
892
|
const untilTime = new Date(query.until).getTime();
|
|
869
|
-
|
|
870
|
-
(
|
|
893
|
+
trajectories = trajectories.filter(
|
|
894
|
+
(trajectory) => new Date(trajectory.startedAt).getTime() <= untilTime
|
|
871
895
|
);
|
|
872
896
|
}
|
|
873
897
|
const sortBy = query.sortBy ?? "startedAt";
|
|
874
898
|
const sortOrder = query.sortOrder ?? "desc";
|
|
875
|
-
|
|
876
|
-
const aVal = a
|
|
877
|
-
const bVal = b
|
|
899
|
+
trajectories.sort((a, b) => {
|
|
900
|
+
const aVal = this.getSortValue(a, sortBy);
|
|
901
|
+
const bVal = this.getSortValue(b, sortBy);
|
|
878
902
|
const cmp = String(aVal).localeCompare(String(bVal));
|
|
879
903
|
return sortOrder === "asc" ? cmp : -cmp;
|
|
880
904
|
});
|
|
881
905
|
const offset = query.offset ?? 0;
|
|
882
906
|
const limit = query.limit ?? 500;
|
|
883
|
-
|
|
884
|
-
return
|
|
885
|
-
entries.map(async ([id, entry]) => {
|
|
886
|
-
const trajectory = await this.get(id);
|
|
887
|
-
return {
|
|
888
|
-
id,
|
|
889
|
-
title: entry.title,
|
|
890
|
-
status: entry.status,
|
|
891
|
-
startedAt: entry.startedAt,
|
|
892
|
-
completedAt: entry.completedAt,
|
|
893
|
-
confidence: trajectory?.retrospective?.confidence,
|
|
894
|
-
chapterCount: trajectory?.chapters.length ?? 0,
|
|
895
|
-
decisionCount: trajectory?.chapters.reduce(
|
|
896
|
-
(count, chapter) => count + chapter.events.filter((e) => e.type === "decision").length,
|
|
897
|
-
0
|
|
898
|
-
) ?? 0
|
|
899
|
-
};
|
|
900
|
-
})
|
|
901
|
-
);
|
|
907
|
+
trajectories = trajectories.slice(offset, offset + limit);
|
|
908
|
+
return trajectories.map((trajectory) => this.toSummary(trajectory));
|
|
902
909
|
}
|
|
903
910
|
/**
|
|
904
911
|
* Delete a trajectory
|
|
905
912
|
*/
|
|
906
913
|
async delete(id) {
|
|
907
|
-
|
|
908
|
-
if (existsSync(activePath)) {
|
|
909
|
-
await unlink(activePath);
|
|
910
|
-
}
|
|
911
|
-
await withIndexLock(this.indexPath, async () => {
|
|
912
|
-
const index = await this.loadIndex();
|
|
913
|
-
const entry = index.trajectories[id];
|
|
914
|
-
if (entry?.path && existsSync(entry.path)) {
|
|
915
|
-
await unlink(entry.path);
|
|
916
|
-
const mdPath = entry.path.replace(".json", ".md");
|
|
917
|
-
if (existsSync(mdPath)) {
|
|
918
|
-
await unlink(mdPath);
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
delete index.trajectories[id];
|
|
922
|
-
await this.saveIndex(index);
|
|
923
|
-
});
|
|
914
|
+
await this.deleteWithSummary(id);
|
|
924
915
|
}
|
|
925
916
|
/**
|
|
926
917
|
* Search trajectories by text
|
|
@@ -953,12 +944,395 @@ var FileStorage = class {
|
|
|
953
944
|
}
|
|
954
945
|
return matches;
|
|
955
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
|
+
}
|
|
956
1035
|
/**
|
|
957
1036
|
* Close storage (no-op for file storage)
|
|
958
1037
|
*/
|
|
959
1038
|
async close() {
|
|
960
1039
|
}
|
|
961
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
|
+
}
|
|
962
1336
|
/**
|
|
963
1337
|
* Read a trajectory file and return a tagged result so callers can
|
|
964
1338
|
* distinguish missing files, malformed JSON, and schema violations.
|
|
@@ -998,82 +1372,25 @@ var FileStorage = class {
|
|
|
998
1372
|
const result = await this.readTrajectoryFile(path2);
|
|
999
1373
|
return result.ok ? result.trajectory : null;
|
|
1000
1374
|
}
|
|
1001
|
-
/**
|
|
1002
|
-
* Read and parse the on-disk index.
|
|
1003
|
-
*
|
|
1004
|
-
* Tolerances (belt-and-braces against the read/write race):
|
|
1005
|
-
* - ENOENT: first-run, return an empty index silently.
|
|
1006
|
-
* - Empty file: a concurrent writer truncated index.json in "w" mode
|
|
1007
|
-
* right before we read. Return an empty index silently — this is
|
|
1008
|
-
* not a real corruption, just an interleaving the mutex + atomic
|
|
1009
|
-
* rename should already prevent. Logging here would be noise.
|
|
1010
|
-
* - Non-empty but malformed JSON: genuinely corrupted on disk (hand
|
|
1011
|
-
* edit, disk error, etc). Log it and return an empty index so the
|
|
1012
|
-
* caller can recover, but keep the log so the problem is visible.
|
|
1013
|
-
*/
|
|
1014
|
-
async loadIndex() {
|
|
1015
|
-
let content;
|
|
1016
|
-
try {
|
|
1017
|
-
content = await readFile(this.indexPath, "utf-8");
|
|
1018
|
-
} catch (error) {
|
|
1019
|
-
if (error.code !== "ENOENT") {
|
|
1020
|
-
console.error(
|
|
1021
|
-
"Error loading trajectory index, using empty index:",
|
|
1022
|
-
error
|
|
1023
|
-
);
|
|
1024
|
-
}
|
|
1025
|
-
return this.emptyIndex();
|
|
1026
|
-
}
|
|
1027
|
-
if (content.length === 0) {
|
|
1028
|
-
return this.emptyIndex();
|
|
1029
|
-
}
|
|
1030
|
-
try {
|
|
1031
|
-
return JSON.parse(content);
|
|
1032
|
-
} catch (error) {
|
|
1033
|
-
console.error(
|
|
1034
|
-
"Error loading trajectory index, using empty index:",
|
|
1035
|
-
error
|
|
1036
|
-
);
|
|
1037
|
-
return this.emptyIndex();
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
emptyIndex() {
|
|
1041
|
-
return {
|
|
1042
|
-
version: 1,
|
|
1043
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1044
|
-
trajectories: {}
|
|
1045
|
-
};
|
|
1046
|
-
}
|
|
1047
|
-
/**
|
|
1048
|
-
* Atomic write: stage into a process-unique temp path in the same directory
|
|
1049
|
-
* and then rename over the live file. `rename` is atomic on POSIX, so
|
|
1050
|
-
* concurrent readers in any process either see the old complete file or
|
|
1051
|
-
* the new complete file — never a half-written / zero-byte state.
|
|
1052
|
-
*
|
|
1053
|
-
* Callers MUST hold `withIndexLock(this.indexPath, ...)` so the in-process
|
|
1054
|
-
* read-modify-write cycle stays serialized; the unique temp name also keeps
|
|
1055
|
-
* parallel writers in other processes from colliding on a shared tmp path.
|
|
1056
|
-
*/
|
|
1057
|
-
async saveIndex(index) {
|
|
1058
|
-
index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1059
|
-
const tmpPath = `${this.indexPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1060
|
-
await writeFile(tmpPath, JSON.stringify(index, null, 2), "utf-8");
|
|
1061
|
-
await rename(tmpPath, this.indexPath);
|
|
1062
|
-
}
|
|
1063
|
-
async updateIndex(trajectory, filePath) {
|
|
1064
|
-
await withIndexLock(this.indexPath, async () => {
|
|
1065
|
-
const index = await this.loadIndex();
|
|
1066
|
-
index.trajectories[trajectory.id] = {
|
|
1067
|
-
title: trajectory.task.title,
|
|
1068
|
-
status: trajectory.status,
|
|
1069
|
-
startedAt: trajectory.startedAt,
|
|
1070
|
-
completedAt: trajectory.completedAt,
|
|
1071
|
-
path: filePath
|
|
1072
|
-
};
|
|
1073
|
-
await this.saveIndex(index);
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1076
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
|
+
}
|
|
1077
1394
|
|
|
1078
1395
|
// src/cli/commands/abandon.ts
|
|
1079
1396
|
function registerAbandonCommand(program2) {
|
|
@@ -1096,14 +1413,8 @@ function registerAbandonCommand(program2) {
|
|
|
1096
1413
|
|
|
1097
1414
|
// src/cli/commands/compact.ts
|
|
1098
1415
|
import { execFileSync } from "child_process";
|
|
1099
|
-
import {
|
|
1100
|
-
|
|
1101
|
-
mkdirSync,
|
|
1102
|
-
readFileSync as readFileSync2,
|
|
1103
|
-
unlinkSync,
|
|
1104
|
-
writeFileSync
|
|
1105
|
-
} from "fs";
|
|
1106
|
-
import { dirname, join as join4 } from "path";
|
|
1416
|
+
import { existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
|
|
1417
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
1107
1418
|
|
|
1108
1419
|
// src/compact/config.ts
|
|
1109
1420
|
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
@@ -1807,7 +2118,7 @@ function buildCliArgs(cli) {
|
|
|
1807
2118
|
}
|
|
1808
2119
|
}
|
|
1809
2120
|
function spawnWithStdin(command, args, input) {
|
|
1810
|
-
return new Promise((
|
|
2121
|
+
return new Promise((resolve2, reject) => {
|
|
1811
2122
|
const child = spawn(command, args, {
|
|
1812
2123
|
timeout: 3e5,
|
|
1813
2124
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1825,7 +2136,7 @@ function spawnWithStdin(command, args, input) {
|
|
|
1825
2136
|
new Error(`CLI exited with code ${code}: ${stderr.slice(0, 200)}`)
|
|
1826
2137
|
);
|
|
1827
2138
|
} else {
|
|
1828
|
-
|
|
2139
|
+
resolve2(Buffer.concat(chunks).toString().trim());
|
|
1829
2140
|
}
|
|
1830
2141
|
});
|
|
1831
2142
|
child.stdin.write(input);
|
|
@@ -2250,7 +2561,7 @@ function registerCompactCommand(program2) {
|
|
|
2250
2561
|
"Comma-separated focus areas to emphasize in LLM compaction"
|
|
2251
2562
|
).option("--markdown", "Also write a Markdown companion file").option("--no-markdown", "Skip writing a Markdown companion file").option(
|
|
2252
2563
|
"--discard-sources",
|
|
2253
|
-
"After saving the compaction, delete source trajectory JSON/MD/trace files
|
|
2564
|
+
"After saving the compaction, delete source trajectory JSON/MD/trace files"
|
|
2254
2565
|
).option("--dry-run", "Preview what would be compacted without saving").option("--output <path>", "Output path for compacted trajectory").action(async (options) => {
|
|
2255
2566
|
const trajectories = await loadTrajectories(options);
|
|
2256
2567
|
if (trajectories.length === 0) {
|
|
@@ -2291,7 +2602,7 @@ function registerCompactCommand(program2) {
|
|
|
2291
2602
|
markdownEnabled
|
|
2292
2603
|
);
|
|
2293
2604
|
if (options.discardSources) {
|
|
2294
|
-
const discardSummary = discardSourceTrajectories(trajectories);
|
|
2605
|
+
const discardSummary = await discardSourceTrajectories(trajectories);
|
|
2295
2606
|
printDiscardSummary(discardSummary);
|
|
2296
2607
|
} else {
|
|
2297
2608
|
await markTrajectoriesAsCompacted(
|
|
@@ -2303,7 +2614,7 @@ function registerCompactCommand(program2) {
|
|
|
2303
2614
|
Compacted trajectory saved to: ${outputPath2}`);
|
|
2304
2615
|
if (markdownEnabled) {
|
|
2305
2616
|
console.log(
|
|
2306
|
-
`Markdown summary saved to: ${
|
|
2617
|
+
`Markdown summary saved to: ${getMarkdownOutputPath2(outputPath2)}`
|
|
2307
2618
|
);
|
|
2308
2619
|
}
|
|
2309
2620
|
printCompactedSummary(mechanicalCompacted);
|
|
@@ -2352,7 +2663,7 @@ Compacted trajectory saved to: ${outputPath2}`);
|
|
|
2352
2663
|
const outputPath = options.output || getDefaultOutputPath(compacted, options.workflow);
|
|
2353
2664
|
saveCompactionArtifacts(compacted, outputPath, markdownEnabled);
|
|
2354
2665
|
if (options.discardSources) {
|
|
2355
|
-
const discardSummary = discardSourceTrajectories(trajectories);
|
|
2666
|
+
const discardSummary = await discardSourceTrajectories(trajectories);
|
|
2356
2667
|
printDiscardSummary(discardSummary);
|
|
2357
2668
|
} else {
|
|
2358
2669
|
await markTrajectoriesAsCompacted(trajectories, compacted.id);
|
|
@@ -2361,7 +2672,7 @@ Compacted trajectory saved to: ${outputPath2}`);
|
|
|
2361
2672
|
Compacted trajectory saved to: ${outputPath}`);
|
|
2362
2673
|
if (markdownEnabled) {
|
|
2363
2674
|
console.log(
|
|
2364
|
-
`Markdown summary saved to: ${
|
|
2675
|
+
`Markdown summary saved to: ${getMarkdownOutputPath2(outputPath)}`
|
|
2365
2676
|
);
|
|
2366
2677
|
}
|
|
2367
2678
|
printCompactedSummary(compacted);
|
|
@@ -2380,19 +2691,12 @@ async function loadTrajectories(options) {
|
|
|
2380
2691
|
return [trimmed, trimmed.slice(0, 7)];
|
|
2381
2692
|
})
|
|
2382
2693
|
) : null;
|
|
2383
|
-
const compactedIds = options.all ? /* @__PURE__ */ new Set() : getCompactedTrajectoryIds();
|
|
2694
|
+
const compactedIds = options.all ? /* @__PURE__ */ new Set() : await getCompactedTrajectoryIds();
|
|
2384
2695
|
const searchPaths = getSearchPaths();
|
|
2385
2696
|
const seenIds = /* @__PURE__ */ new Set();
|
|
2386
2697
|
for (const searchPath of searchPaths) {
|
|
2387
2698
|
if (!existsSync3(searchPath)) continue;
|
|
2388
|
-
const
|
|
2389
|
-
process.env.TRAJECTORIES_DATA_DIR = searchPath;
|
|
2390
|
-
const storage = new FileStorage();
|
|
2391
|
-
if (originalDataDir !== void 0) {
|
|
2392
|
-
process.env.TRAJECTORIES_DATA_DIR = originalDataDir;
|
|
2393
|
-
} else {
|
|
2394
|
-
delete process.env.TRAJECTORIES_DATA_DIR;
|
|
2395
|
-
}
|
|
2699
|
+
const storage = createStorageForSearchPath(searchPath);
|
|
2396
2700
|
await storage.initialize();
|
|
2397
2701
|
const summaries = await storage.list({
|
|
2398
2702
|
status: "completed",
|
|
@@ -2438,6 +2742,19 @@ async function loadTrajectories(options) {
|
|
|
2438
2742
|
}
|
|
2439
2743
|
return trajectories;
|
|
2440
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
|
+
}
|
|
2441
2758
|
function getBranchCommits(targetBranch) {
|
|
2442
2759
|
const commits = /* @__PURE__ */ new Set();
|
|
2443
2760
|
try {
|
|
@@ -2462,21 +2779,17 @@ function getBranchCommits(targetBranch) {
|
|
|
2462
2779
|
}
|
|
2463
2780
|
return commits;
|
|
2464
2781
|
}
|
|
2465
|
-
function getCompactedTrajectoryIds() {
|
|
2782
|
+
async function getCompactedTrajectoryIds() {
|
|
2466
2783
|
const compacted = /* @__PURE__ */ new Set();
|
|
2467
2784
|
const searchPaths = getSearchPaths();
|
|
2468
2785
|
for (const searchPath of searchPaths) {
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
compacted.add(id);
|
|
2477
|
-
}
|
|
2478
|
-
}
|
|
2479
|
-
} 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);
|
|
2480
2793
|
}
|
|
2481
2794
|
}
|
|
2482
2795
|
return compacted;
|
|
@@ -2484,92 +2797,45 @@ function getCompactedTrajectoryIds() {
|
|
|
2484
2797
|
async function markTrajectoriesAsCompacted(trajectories, compactedIntoId) {
|
|
2485
2798
|
const searchPaths = getSearchPaths();
|
|
2486
2799
|
for (const searchPath of searchPaths) {
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
try {
|
|
2490
|
-
const indexContent = readFileSync2(indexPath, "utf-8");
|
|
2491
|
-
const index = JSON.parse(indexContent);
|
|
2492
|
-
let updated = false;
|
|
2493
|
-
for (const traj of trajectories) {
|
|
2494
|
-
if (index.trajectories[traj.id]) {
|
|
2495
|
-
index.trajectories[traj.id].compactedInto = compactedIntoId;
|
|
2496
|
-
updated = true;
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
if (updated) {
|
|
2500
|
-
index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
2501
|
-
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
|
2502
|
-
}
|
|
2503
|
-
} catch {
|
|
2800
|
+
if (!existsSync3(searchPath)) {
|
|
2801
|
+
continue;
|
|
2504
2802
|
}
|
|
2803
|
+
const storage = createStorageForSearchPath(searchPath);
|
|
2804
|
+
await storage.initialize();
|
|
2805
|
+
await storage.markCompactedMany(
|
|
2806
|
+
trajectories.map((trajectory) => trajectory.id),
|
|
2807
|
+
compactedIntoId
|
|
2808
|
+
);
|
|
2505
2809
|
}
|
|
2506
2810
|
}
|
|
2507
|
-
function discardSourceTrajectories(trajectories) {
|
|
2508
|
-
const sourceIds = new Set(trajectories.map((trajectory) => trajectory.id));
|
|
2811
|
+
async function discardSourceTrajectories(trajectories) {
|
|
2509
2812
|
const summary = {
|
|
2510
|
-
|
|
2813
|
+
removedTrajectories: 0,
|
|
2511
2814
|
deletedJsonFiles: 0,
|
|
2512
2815
|
deletedMarkdownFiles: 0,
|
|
2513
|
-
deletedTraceFiles: 0
|
|
2816
|
+
deletedTraceFiles: 0,
|
|
2817
|
+
deletedCompactionFiles: 0
|
|
2514
2818
|
};
|
|
2515
2819
|
for (const searchPath of getSearchPaths()) {
|
|
2516
|
-
|
|
2517
|
-
if (!existsSync3(indexPath)) continue;
|
|
2518
|
-
let index;
|
|
2519
|
-
try {
|
|
2520
|
-
const indexContent = readFileSync2(indexPath, "utf-8");
|
|
2521
|
-
const parsedIndex = JSON.parse(indexContent);
|
|
2522
|
-
if (!isTrajectoryIndex(parsedIndex)) {
|
|
2523
|
-
continue;
|
|
2524
|
-
}
|
|
2525
|
-
index = parsedIndex;
|
|
2526
|
-
} catch {
|
|
2820
|
+
if (!existsSync3(searchPath)) {
|
|
2527
2821
|
continue;
|
|
2528
2822
|
}
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
if (deleteFileIfExists(getTraceOutputPath(entry.path))) {
|
|
2540
|
-
summary.deletedTraceFiles += 1;
|
|
2541
|
-
}
|
|
2542
|
-
delete index.trajectories[id];
|
|
2543
|
-
summary.removedIndexEntries += 1;
|
|
2544
|
-
updated = true;
|
|
2545
|
-
}
|
|
2546
|
-
if (updated) {
|
|
2547
|
-
index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
2548
|
-
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
|
2549
|
-
}
|
|
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;
|
|
2550
2833
|
}
|
|
2551
2834
|
return summary;
|
|
2552
2835
|
}
|
|
2553
|
-
function deleteFileIfExists(path2) {
|
|
2554
|
-
if (!existsSync3(path2)) {
|
|
2555
|
-
return false;
|
|
2556
|
-
}
|
|
2557
|
-
unlinkSync(path2);
|
|
2558
|
-
return true;
|
|
2559
|
-
}
|
|
2560
|
-
function isTrajectoryIndex(value) {
|
|
2561
|
-
if (value === null || typeof value !== "object") {
|
|
2562
|
-
return false;
|
|
2563
|
-
}
|
|
2564
|
-
const candidate = value;
|
|
2565
|
-
return candidate.trajectories !== null && typeof candidate.trajectories === "object" && !Array.isArray(candidate.trajectories);
|
|
2566
|
-
}
|
|
2567
|
-
function getTraceOutputPath(outputPath) {
|
|
2568
|
-
return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".trace.json") : `${outputPath}.trace.json`;
|
|
2569
|
-
}
|
|
2570
2836
|
function printDiscardSummary(summary) {
|
|
2571
2837
|
console.log(
|
|
2572
|
-
`Discarded source trajectories: ${summary.
|
|
2838
|
+
`Discarded source trajectories: ${summary.removedTrajectories} trajectories, ${summary.deletedJsonFiles} JSON files, ${summary.deletedMarkdownFiles} Markdown files, ${summary.deletedTraceFiles} trace files, ${summary.deletedCompactionFiles} compaction markers`
|
|
2573
2839
|
);
|
|
2574
2840
|
}
|
|
2575
2841
|
function parseRelativeDate(input) {
|
|
@@ -2807,19 +3073,19 @@ function getDefaultOutputPath(compacted, workflowId) {
|
|
|
2807
3073
|
return join4(compactedDir, `${compacted.id}_${dateStr}.json`);
|
|
2808
3074
|
}
|
|
2809
3075
|
function saveCompactionArtifacts(compacted, outputPath, markdownEnabled) {
|
|
2810
|
-
const dir =
|
|
3076
|
+
const dir = dirname2(outputPath);
|
|
2811
3077
|
if (!existsSync3(dir)) {
|
|
2812
3078
|
mkdirSync(dir, { recursive: true });
|
|
2813
3079
|
}
|
|
2814
3080
|
writeFileSync(outputPath, JSON.stringify(compacted, null, 2));
|
|
2815
3081
|
if (markdownEnabled) {
|
|
2816
3082
|
writeFileSync(
|
|
2817
|
-
|
|
3083
|
+
getMarkdownOutputPath2(outputPath),
|
|
2818
3084
|
renderCompactionMarkdown(compacted)
|
|
2819
3085
|
);
|
|
2820
3086
|
}
|
|
2821
3087
|
}
|
|
2822
|
-
function
|
|
3088
|
+
function getMarkdownOutputPath2(outputPath) {
|
|
2823
3089
|
return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".md") : `${outputPath}.md`;
|
|
2824
3090
|
}
|
|
2825
3091
|
function renderCompactionMarkdown(compacted) {
|
|
@@ -3142,7 +3408,7 @@ function createTraceRef(startRef, traceId) {
|
|
|
3142
3408
|
|
|
3143
3409
|
// src/core/trailers.ts
|
|
3144
3410
|
import { execSync as execSync2 } from "child_process";
|
|
3145
|
-
import { readFileSync as
|
|
3411
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
3146
3412
|
function getCommitsBetween(startRef, endRef = "HEAD") {
|
|
3147
3413
|
if (!isGitRepo()) {
|
|
3148
3414
|
return [];
|
|
@@ -3255,7 +3521,7 @@ function detectExistingHook() {
|
|
|
3255
3521
|
}).trim();
|
|
3256
3522
|
const hookPath = `${hooksDir}/hooks/prepare-commit-msg`;
|
|
3257
3523
|
try {
|
|
3258
|
-
const content =
|
|
3524
|
+
const content = readFileSync2(hookPath, "utf-8");
|
|
3259
3525
|
if (content.includes("agent-trajectories")) {
|
|
3260
3526
|
return "ours";
|
|
3261
3527
|
}
|
|
@@ -3393,6 +3659,51 @@ function registerDecisionCommand(program2) {
|
|
|
3393
3659
|
});
|
|
3394
3660
|
}
|
|
3395
3661
|
|
|
3662
|
+
// src/cli/commands/doctor.ts
|
|
3663
|
+
function registerDoctorCommand(program2) {
|
|
3664
|
+
program2.command("doctor").description(
|
|
3665
|
+
"List trajectory files that fail to load; optionally quarantine them"
|
|
3666
|
+
).option(
|
|
3667
|
+
"--quarantine",
|
|
3668
|
+
"Move invalid files to .trajectories/invalid/ so reconcile stops scanning them"
|
|
3669
|
+
).action(async (opts) => {
|
|
3670
|
+
const storage = new FileStorage();
|
|
3671
|
+
await storage.initialize();
|
|
3672
|
+
const summary = storage.getLastReconcileSummary();
|
|
3673
|
+
const failures = summary?.failures ?? [];
|
|
3674
|
+
if (failures.length === 0) {
|
|
3675
|
+
console.log("No invalid trajectory files found.");
|
|
3676
|
+
return;
|
|
3677
|
+
}
|
|
3678
|
+
console.log(`Found ${failures.length} invalid trajectory file(s):`);
|
|
3679
|
+
for (const failure of failures) {
|
|
3680
|
+
console.log(` ${failure.path}`);
|
|
3681
|
+
console.log(` reason: ${failure.reason}`);
|
|
3682
|
+
console.log(` detail: ${failure.message}`);
|
|
3683
|
+
}
|
|
3684
|
+
if (!opts.quarantine) {
|
|
3685
|
+
console.log(
|
|
3686
|
+
"\nRun `trail doctor --quarantine` to move these files into .trajectories/invalid/."
|
|
3687
|
+
);
|
|
3688
|
+
return;
|
|
3689
|
+
}
|
|
3690
|
+
const result = await storage.quarantineInvalid();
|
|
3691
|
+
if (result.moved.length === 0) {
|
|
3692
|
+
console.log(
|
|
3693
|
+
"\nNo files were moved (io_error failures are not auto-quarantined)."
|
|
3694
|
+
);
|
|
3695
|
+
return;
|
|
3696
|
+
}
|
|
3697
|
+
console.log(
|
|
3698
|
+
`
|
|
3699
|
+
Moved ${result.moved.length} file(s) to ${result.targetDir}:`
|
|
3700
|
+
);
|
|
3701
|
+
for (const failure of result.moved) {
|
|
3702
|
+
console.log(` ${failure.path}`);
|
|
3703
|
+
}
|
|
3704
|
+
});
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3396
3707
|
// src/cli/commands/enable.ts
|
|
3397
3708
|
import { execSync as execSync3 } from "child_process";
|
|
3398
3709
|
import { existsSync as existsSync5 } from "fs";
|
|
@@ -3470,8 +3781,8 @@ function registerEnableCommand(program2) {
|
|
|
3470
3781
|
console.error("Remove it manually if needed");
|
|
3471
3782
|
throw new Error("Hook not ours");
|
|
3472
3783
|
}
|
|
3473
|
-
const { unlink
|
|
3474
|
-
await
|
|
3784
|
+
const { unlink } = await import("fs/promises");
|
|
3785
|
+
await unlink(hookPath);
|
|
3475
3786
|
console.log("Trajectory hook removed");
|
|
3476
3787
|
});
|
|
3477
3788
|
}
|
|
@@ -4627,9 +4938,32 @@ function registerStartCommand(program2) {
|
|
|
4627
4938
|
|
|
4628
4939
|
// src/cli/commands/status.ts
|
|
4629
4940
|
function registerStatusCommand(program2) {
|
|
4630
|
-
program2.command("status").description("Show active trajectory status").
|
|
4941
|
+
program2.command("status").description("Show active trajectory status").option(
|
|
4942
|
+
"-v, --verbose",
|
|
4943
|
+
"Show paths and validation errors for any trajectory files that failed to load"
|
|
4944
|
+
).action(async (opts) => {
|
|
4631
4945
|
const storage = new FileStorage();
|
|
4632
4946
|
await storage.initialize();
|
|
4947
|
+
if (opts.verbose) {
|
|
4948
|
+
const summary = storage.getLastReconcileSummary();
|
|
4949
|
+
const failures = summary?.failures ?? [];
|
|
4950
|
+
if (failures.length === 0) {
|
|
4951
|
+
console.log("All trajectory files loaded cleanly.");
|
|
4952
|
+
} else {
|
|
4953
|
+
console.log(
|
|
4954
|
+
`Skipped ${failures.length} trajectory file(s) during reconcile:`
|
|
4955
|
+
);
|
|
4956
|
+
for (const failure of failures) {
|
|
4957
|
+
console.log(` ${failure.path}`);
|
|
4958
|
+
console.log(` reason: ${failure.reason}`);
|
|
4959
|
+
console.log(` detail: ${failure.message}`);
|
|
4960
|
+
}
|
|
4961
|
+
console.log(
|
|
4962
|
+
"\nRun `trail doctor --quarantine` to move them aside and silence this warning."
|
|
4963
|
+
);
|
|
4964
|
+
}
|
|
4965
|
+
console.log("");
|
|
4966
|
+
}
|
|
4633
4967
|
const active = await storage.getActive();
|
|
4634
4968
|
if (!active) {
|
|
4635
4969
|
console.log("No active trajectory");
|
|
@@ -4702,6 +5036,7 @@ function registerCommands(program2) {
|
|
|
4702
5036
|
registerExportCommand(program2);
|
|
4703
5037
|
registerEnableCommand(program2);
|
|
4704
5038
|
registerCompactCommand(program2);
|
|
5039
|
+
registerDoctorCommand(program2);
|
|
4705
5040
|
}
|
|
4706
5041
|
|
|
4707
5042
|
// src/cli/version.ts
|