agent-trajectories 0.5.6 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-2XT3DOJC.js → chunk-WMJRBQB4.js} +227 -76
- package/dist/chunk-WMJRBQB4.js.map +1 -0
- package/dist/cli/index.js +296 -76
- package/dist/cli/index.js.map +1 -1
- package/dist/{index-thTh5iI8.d.ts → index-C9IcYSNQ.d.ts} +76 -1
- 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-2XT3DOJC.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -404,9 +404,17 @@ function abandonTrajectory(trajectory, reason) {
|
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
// src/storage/file.ts
|
|
407
|
+
import { randomUUID } from "crypto";
|
|
407
408
|
import { existsSync } from "fs";
|
|
408
|
-
import {
|
|
409
|
-
|
|
409
|
+
import {
|
|
410
|
+
mkdir,
|
|
411
|
+
readFile,
|
|
412
|
+
readdir,
|
|
413
|
+
rename,
|
|
414
|
+
unlink,
|
|
415
|
+
writeFile
|
|
416
|
+
} from "fs/promises";
|
|
417
|
+
import { basename, dirname, isAbsolute, join, relative } from "path";
|
|
410
418
|
|
|
411
419
|
// src/export/markdown.ts
|
|
412
420
|
function exportToMarkdown(trajectory) {
|
|
@@ -586,12 +594,37 @@ function getSearchPaths() {
|
|
|
586
594
|
}
|
|
587
595
|
return [join(process.cwd(), ".trajectories")];
|
|
588
596
|
}
|
|
597
|
+
function describeReadFailure(reason, error) {
|
|
598
|
+
if (reason === "schema_violation" && error && typeof error === "object" && "issues" in error) {
|
|
599
|
+
const issues = error.issues ?? [];
|
|
600
|
+
if (issues.length > 0) {
|
|
601
|
+
const first = issues[0];
|
|
602
|
+
const where = first.path.length > 0 ? first.path.join(".") : "root";
|
|
603
|
+
const extra = issues.length > 1 ? ` (+${issues.length - 1} more)` : "";
|
|
604
|
+
return `${where}: ${first.message}${extra}`;
|
|
605
|
+
}
|
|
606
|
+
return "schema validation failed";
|
|
607
|
+
}
|
|
608
|
+
if (error instanceof Error) return error.message;
|
|
609
|
+
return String(error);
|
|
610
|
+
}
|
|
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
|
+
}
|
|
589
621
|
var FileStorage = class {
|
|
590
622
|
baseDir;
|
|
591
623
|
trajectoriesDir;
|
|
592
624
|
activeDir;
|
|
593
625
|
completedDir;
|
|
594
626
|
indexPath;
|
|
627
|
+
lastReconcileSummary;
|
|
595
628
|
constructor(baseDir) {
|
|
596
629
|
this.baseDir = baseDir ?? process.cwd();
|
|
597
630
|
const dataDir = process.env.TRAJECTORIES_DATA_DIR;
|
|
@@ -612,10 +645,10 @@ var FileStorage = class {
|
|
|
612
645
|
await mkdir(this.activeDir, { recursive: true });
|
|
613
646
|
await mkdir(this.completedDir, { recursive: true });
|
|
614
647
|
if (!existsSync(this.indexPath)) {
|
|
615
|
-
await this.
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
648
|
+
await withIndexLock(this.indexPath, async () => {
|
|
649
|
+
if (!existsSync(this.indexPath)) {
|
|
650
|
+
await this.saveIndex(this.emptyIndex());
|
|
651
|
+
}
|
|
619
652
|
});
|
|
620
653
|
}
|
|
621
654
|
await this.reconcileIndex();
|
|
@@ -641,51 +674,59 @@ var FileStorage = class {
|
|
|
641
674
|
alreadyIndexed: 0,
|
|
642
675
|
skippedMalformedJson: 0,
|
|
643
676
|
skippedSchemaViolation: 0,
|
|
644
|
-
skippedIoError: 0
|
|
677
|
+
skippedIoError: 0,
|
|
678
|
+
failures: []
|
|
645
679
|
};
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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;
|
|
654
692
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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;
|
|
669
711
|
}
|
|
670
|
-
|
|
712
|
+
const trajectory = result.trajectory;
|
|
713
|
+
if (index.trajectories[trajectory.id]) {
|
|
714
|
+
summary.alreadyIndexed += 1;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
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;
|
|
671
725
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
summary.alreadyIndexed += 1;
|
|
675
|
-
continue;
|
|
726
|
+
if (Object.keys(index.trajectories).length !== before) {
|
|
727
|
+
await this.saveIndex(index);
|
|
676
728
|
}
|
|
677
|
-
|
|
678
|
-
title: trajectory.task.title,
|
|
679
|
-
status: trajectory.status,
|
|
680
|
-
startedAt: trajectory.startedAt,
|
|
681
|
-
completedAt: trajectory.completedAt,
|
|
682
|
-
path: filePath
|
|
683
|
-
};
|
|
684
|
-
summary.added += 1;
|
|
685
|
-
}
|
|
686
|
-
if (Object.keys(index.trajectories).length !== before) {
|
|
687
|
-
await this.saveIndex(index);
|
|
688
|
-
}
|
|
729
|
+
});
|
|
689
730
|
const hadSkips = summary.skippedMalformedJson + summary.skippedSchemaViolation + summary.skippedIoError > 0;
|
|
690
731
|
if (summary.added > 0 || hadSkips) {
|
|
691
732
|
const parts = [`reconciled ${summary.added}/${summary.scanned}`];
|
|
@@ -700,8 +741,74 @@ var FileStorage = class {
|
|
|
700
741
|
}
|
|
701
742
|
console.warn(`[trajectories] ${parts.join(", ")}`);
|
|
702
743
|
}
|
|
744
|
+
this.lastReconcileSummary = summary;
|
|
703
745
|
return summary;
|
|
704
746
|
}
|
|
747
|
+
/**
|
|
748
|
+
* Returns the most recent reconcile summary, if any. Lets the CLI
|
|
749
|
+
* inspect the failures collected during `initialize()` without having
|
|
750
|
+
* to re-walk the directory tree (and re-emit the warn line).
|
|
751
|
+
*/
|
|
752
|
+
getLastReconcileSummary() {
|
|
753
|
+
return this.lastReconcileSummary;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Move trajectory files that fail to load into `.trajectories/invalid/`
|
|
757
|
+
* so reconcile no longer scans them. Only quarantines parse and schema
|
|
758
|
+
* failures — transient io_error failures are left in place because the
|
|
759
|
+
* file may load fine on the next attempt.
|
|
760
|
+
*
|
|
761
|
+
* Returns the list of files that were moved (with their original paths
|
|
762
|
+
* and the destination directory) so the caller can report what changed.
|
|
763
|
+
*/
|
|
764
|
+
async quarantineInvalid() {
|
|
765
|
+
const summary = await this.reconcileIndex();
|
|
766
|
+
const targetDir = join(this.trajectoriesDir, "invalid");
|
|
767
|
+
const candidates = summary.failures.filter((f) => f.reason !== "io_error");
|
|
768
|
+
if (candidates.length === 0) {
|
|
769
|
+
return { moved: [], targetDir };
|
|
770
|
+
}
|
|
771
|
+
await mkdir(targetDir, { recursive: true });
|
|
772
|
+
const moved = [];
|
|
773
|
+
for (const failure of candidates) {
|
|
774
|
+
const dest = await this.resolveQuarantineDest(failure.path, targetDir);
|
|
775
|
+
try {
|
|
776
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
777
|
+
await rename(failure.path, dest);
|
|
778
|
+
moved.push(failure);
|
|
779
|
+
} catch (error) {
|
|
780
|
+
console.warn(
|
|
781
|
+
`[trajectories] failed to quarantine ${failure.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return { moved, targetDir };
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Pick a destination path under `targetDir` for a quarantined file.
|
|
789
|
+
*
|
|
790
|
+
* Preserves the file's relative location under the trajectories root
|
|
791
|
+
* (e.g. `completed/2026-04/foo.json` → `invalid/completed/2026-04/foo.json`)
|
|
792
|
+
* so two invalid files that share a basename across `active/` and
|
|
793
|
+
* `completed/` don't collapse onto each other and silently overwrite.
|
|
794
|
+
*
|
|
795
|
+
* Falls back to a numeric-suffix scheme for paths that live outside
|
|
796
|
+
* the trajectories directory or that, after relative resolution, would
|
|
797
|
+
* still collide with something already quarantined.
|
|
798
|
+
*/
|
|
799
|
+
async resolveQuarantineDest(sourcePath, targetDir) {
|
|
800
|
+
const rel = relative(this.trajectoriesDir, sourcePath);
|
|
801
|
+
const safeRel = rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : basename(sourcePath);
|
|
802
|
+
let dest = join(targetDir, safeRel);
|
|
803
|
+
if (!existsSync(dest)) return dest;
|
|
804
|
+
const ext = safeRel.endsWith(".json") ? ".json" : "";
|
|
805
|
+
const stem = ext ? safeRel.slice(0, -ext.length) : safeRel;
|
|
806
|
+
for (let i = 1; i < 1e3; i += 1) {
|
|
807
|
+
dest = join(targetDir, `${stem}.${i}${ext}`);
|
|
808
|
+
if (!existsSync(dest)) return dest;
|
|
809
|
+
}
|
|
810
|
+
return dest;
|
|
811
|
+
}
|
|
705
812
|
/**
|
|
706
813
|
* Recursively collect all .json file paths under `dir` into `out`.
|
|
707
814
|
* Silently treats a missing directory as empty.
|
|
@@ -888,17 +995,19 @@ var FileStorage = class {
|
|
|
888
995
|
if (existsSync(activePath)) {
|
|
889
996
|
await unlink(activePath);
|
|
890
997
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
+
}
|
|
898
1007
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1008
|
+
delete index.trajectories[id];
|
|
1009
|
+
await this.saveIndex(index);
|
|
1010
|
+
});
|
|
902
1011
|
}
|
|
903
1012
|
/**
|
|
904
1013
|
* Search trajectories by text
|
|
@@ -976,10 +1085,23 @@ var FileStorage = class {
|
|
|
976
1085
|
const result = await this.readTrajectoryFile(path2);
|
|
977
1086
|
return result.ok ? result.trajectory : null;
|
|
978
1087
|
}
|
|
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
|
+
*/
|
|
979
1101
|
async loadIndex() {
|
|
1102
|
+
let content;
|
|
980
1103
|
try {
|
|
981
|
-
|
|
982
|
-
return JSON.parse(content);
|
|
1104
|
+
content = await readFile(this.indexPath, "utf-8");
|
|
983
1105
|
} catch (error) {
|
|
984
1106
|
if (error.code !== "ENOENT") {
|
|
985
1107
|
console.error(
|
|
@@ -987,27 +1109,56 @@ var FileStorage = class {
|
|
|
987
1109
|
error
|
|
988
1110
|
);
|
|
989
1111
|
}
|
|
990
|
-
return
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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();
|
|
995
1125
|
}
|
|
996
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
|
+
*/
|
|
997
1144
|
async saveIndex(index) {
|
|
998
1145
|
index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
999
|
-
|
|
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);
|
|
1000
1149
|
}
|
|
1001
1150
|
async updateIndex(trajectory, filePath) {
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
+
});
|
|
1011
1162
|
}
|
|
1012
1163
|
};
|
|
1013
1164
|
|
|
@@ -1039,7 +1190,7 @@ import {
|
|
|
1039
1190
|
unlinkSync,
|
|
1040
1191
|
writeFileSync
|
|
1041
1192
|
} from "fs";
|
|
1042
|
-
import { dirname, join as join4 } from "path";
|
|
1193
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
1043
1194
|
|
|
1044
1195
|
// src/compact/config.ts
|
|
1045
1196
|
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
@@ -2743,7 +2894,7 @@ function getDefaultOutputPath(compacted, workflowId) {
|
|
|
2743
2894
|
return join4(compactedDir, `${compacted.id}_${dateStr}.json`);
|
|
2744
2895
|
}
|
|
2745
2896
|
function saveCompactionArtifacts(compacted, outputPath, markdownEnabled) {
|
|
2746
|
-
const dir =
|
|
2897
|
+
const dir = dirname2(outputPath);
|
|
2747
2898
|
if (!existsSync3(dir)) {
|
|
2748
2899
|
mkdirSync(dir, { recursive: true });
|
|
2749
2900
|
}
|
|
@@ -3329,6 +3480,51 @@ function registerDecisionCommand(program2) {
|
|
|
3329
3480
|
});
|
|
3330
3481
|
}
|
|
3331
3482
|
|
|
3483
|
+
// src/cli/commands/doctor.ts
|
|
3484
|
+
function registerDoctorCommand(program2) {
|
|
3485
|
+
program2.command("doctor").description(
|
|
3486
|
+
"List trajectory files that fail to load; optionally quarantine them"
|
|
3487
|
+
).option(
|
|
3488
|
+
"--quarantine",
|
|
3489
|
+
"Move invalid files to .trajectories/invalid/ so reconcile stops scanning them"
|
|
3490
|
+
).action(async (opts) => {
|
|
3491
|
+
const storage = new FileStorage();
|
|
3492
|
+
await storage.initialize();
|
|
3493
|
+
const summary = storage.getLastReconcileSummary();
|
|
3494
|
+
const failures = summary?.failures ?? [];
|
|
3495
|
+
if (failures.length === 0) {
|
|
3496
|
+
console.log("No invalid trajectory files found.");
|
|
3497
|
+
return;
|
|
3498
|
+
}
|
|
3499
|
+
console.log(`Found ${failures.length} invalid trajectory file(s):`);
|
|
3500
|
+
for (const failure of failures) {
|
|
3501
|
+
console.log(` ${failure.path}`);
|
|
3502
|
+
console.log(` reason: ${failure.reason}`);
|
|
3503
|
+
console.log(` detail: ${failure.message}`);
|
|
3504
|
+
}
|
|
3505
|
+
if (!opts.quarantine) {
|
|
3506
|
+
console.log(
|
|
3507
|
+
"\nRun `trail doctor --quarantine` to move these files into .trajectories/invalid/."
|
|
3508
|
+
);
|
|
3509
|
+
return;
|
|
3510
|
+
}
|
|
3511
|
+
const result = await storage.quarantineInvalid();
|
|
3512
|
+
if (result.moved.length === 0) {
|
|
3513
|
+
console.log(
|
|
3514
|
+
"\nNo files were moved (io_error failures are not auto-quarantined)."
|
|
3515
|
+
);
|
|
3516
|
+
return;
|
|
3517
|
+
}
|
|
3518
|
+
console.log(
|
|
3519
|
+
`
|
|
3520
|
+
Moved ${result.moved.length} file(s) to ${result.targetDir}:`
|
|
3521
|
+
);
|
|
3522
|
+
for (const failure of result.moved) {
|
|
3523
|
+
console.log(` ${failure.path}`);
|
|
3524
|
+
}
|
|
3525
|
+
});
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3332
3528
|
// src/cli/commands/enable.ts
|
|
3333
3529
|
import { execSync as execSync3 } from "child_process";
|
|
3334
3530
|
import { existsSync as existsSync5 } from "fs";
|
|
@@ -4563,9 +4759,32 @@ function registerStartCommand(program2) {
|
|
|
4563
4759
|
|
|
4564
4760
|
// src/cli/commands/status.ts
|
|
4565
4761
|
function registerStatusCommand(program2) {
|
|
4566
|
-
program2.command("status").description("Show active trajectory status").
|
|
4762
|
+
program2.command("status").description("Show active trajectory status").option(
|
|
4763
|
+
"-v, --verbose",
|
|
4764
|
+
"Show paths and validation errors for any trajectory files that failed to load"
|
|
4765
|
+
).action(async (opts) => {
|
|
4567
4766
|
const storage = new FileStorage();
|
|
4568
4767
|
await storage.initialize();
|
|
4768
|
+
if (opts.verbose) {
|
|
4769
|
+
const summary = storage.getLastReconcileSummary();
|
|
4770
|
+
const failures = summary?.failures ?? [];
|
|
4771
|
+
if (failures.length === 0) {
|
|
4772
|
+
console.log("All trajectory files loaded cleanly.");
|
|
4773
|
+
} else {
|
|
4774
|
+
console.log(
|
|
4775
|
+
`Skipped ${failures.length} trajectory file(s) during reconcile:`
|
|
4776
|
+
);
|
|
4777
|
+
for (const failure of failures) {
|
|
4778
|
+
console.log(` ${failure.path}`);
|
|
4779
|
+
console.log(` reason: ${failure.reason}`);
|
|
4780
|
+
console.log(` detail: ${failure.message}`);
|
|
4781
|
+
}
|
|
4782
|
+
console.log(
|
|
4783
|
+
"\nRun `trail doctor --quarantine` to move them aside and silence this warning."
|
|
4784
|
+
);
|
|
4785
|
+
}
|
|
4786
|
+
console.log("");
|
|
4787
|
+
}
|
|
4569
4788
|
const active = await storage.getActive();
|
|
4570
4789
|
if (!active) {
|
|
4571
4790
|
console.log("No active trajectory");
|
|
@@ -4638,6 +4857,7 @@ function registerCommands(program2) {
|
|
|
4638
4857
|
registerExportCommand(program2);
|
|
4639
4858
|
registerEnableCommand(program2);
|
|
4640
4859
|
registerCompactCommand(program2);
|
|
4860
|
+
registerDoctorCommand(program2);
|
|
4641
4861
|
}
|
|
4642
4862
|
|
|
4643
4863
|
// src/cli/version.ts
|