agent-trajectories 0.5.7 → 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/cli/index.js CHANGED
@@ -414,7 +414,7 @@ import {
414
414
  unlink,
415
415
  writeFile
416
416
  } from "fs/promises";
417
- import { join } from "path";
417
+ import { basename, dirname, isAbsolute, join, relative } from "path";
418
418
 
419
419
  // src/export/markdown.ts
420
420
  function exportToMarkdown(trajectory) {
@@ -594,6 +594,20 @@ function getSearchPaths() {
594
594
  }
595
595
  return [join(process.cwd(), ".trajectories")];
596
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
+ }
597
611
  var indexLocks = /* @__PURE__ */ new Map();
598
612
  function withIndexLock(path2, task) {
599
613
  const prev = indexLocks.get(path2) ?? Promise.resolve();
@@ -610,6 +624,7 @@ var FileStorage = class {
610
624
  activeDir;
611
625
  completedDir;
612
626
  indexPath;
627
+ lastReconcileSummary;
613
628
  constructor(baseDir) {
614
629
  this.baseDir = baseDir ?? process.cwd();
615
630
  const dataDir = process.env.TRAJECTORIES_DATA_DIR;
@@ -659,7 +674,8 @@ var FileStorage = class {
659
674
  alreadyIndexed: 0,
660
675
  skippedMalformedJson: 0,
661
676
  skippedSchemaViolation: 0,
662
- skippedIoError: 0
677
+ skippedIoError: 0,
678
+ failures: []
663
679
  };
664
680
  await withIndexLock(this.indexPath, async () => {
665
681
  const index = await this.loadIndex();
@@ -686,6 +702,11 @@ var FileStorage = class {
686
702
  } else {
687
703
  summary.skippedIoError += 1;
688
704
  }
705
+ summary.failures.push({
706
+ path: result.path,
707
+ reason: result.reason,
708
+ message: describeReadFailure(result.reason, result.error)
709
+ });
689
710
  continue;
690
711
  }
691
712
  const trajectory = result.trajectory;
@@ -720,8 +741,74 @@ var FileStorage = class {
720
741
  }
721
742
  console.warn(`[trajectories] ${parts.join(", ")}`);
722
743
  }
744
+ this.lastReconcileSummary = summary;
723
745
  return summary;
724
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
+ }
725
812
  /**
726
813
  * Recursively collect all .json file paths under `dir` into `out`.
727
814
  * Silently treats a missing directory as empty.
@@ -1103,7 +1190,7 @@ import {
1103
1190
  unlinkSync,
1104
1191
  writeFileSync
1105
1192
  } from "fs";
1106
- import { dirname, join as join4 } from "path";
1193
+ import { dirname as dirname2, join as join4 } from "path";
1107
1194
 
1108
1195
  // src/compact/config.ts
1109
1196
  import { existsSync as existsSync2, readFileSync } from "fs";
@@ -2807,7 +2894,7 @@ function getDefaultOutputPath(compacted, workflowId) {
2807
2894
  return join4(compactedDir, `${compacted.id}_${dateStr}.json`);
2808
2895
  }
2809
2896
  function saveCompactionArtifacts(compacted, outputPath, markdownEnabled) {
2810
- const dir = dirname(outputPath);
2897
+ const dir = dirname2(outputPath);
2811
2898
  if (!existsSync3(dir)) {
2812
2899
  mkdirSync(dir, { recursive: true });
2813
2900
  }
@@ -3393,6 +3480,51 @@ function registerDecisionCommand(program2) {
3393
3480
  });
3394
3481
  }
3395
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
+
3396
3528
  // src/cli/commands/enable.ts
3397
3529
  import { execSync as execSync3 } from "child_process";
3398
3530
  import { existsSync as existsSync5 } from "fs";
@@ -4627,9 +4759,32 @@ function registerStartCommand(program2) {
4627
4759
 
4628
4760
  // src/cli/commands/status.ts
4629
4761
  function registerStatusCommand(program2) {
4630
- program2.command("status").description("Show active trajectory status").action(async () => {
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) => {
4631
4766
  const storage = new FileStorage();
4632
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
+ }
4633
4788
  const active = await storage.getActive();
4634
4789
  if (!active) {
4635
4790
  console.log("No active trajectory");
@@ -4702,6 +4857,7 @@ function registerCommands(program2) {
4702
4857
  registerExportCommand(program2);
4703
4858
  registerEnableCommand(program2);
4704
4859
  registerCompactCommand(program2);
4860
+ registerDoctorCommand(program2);
4705
4861
  }
4706
4862
 
4707
4863
  // src/cli/version.ts