agent-trajectories 0.5.3 → 0.5.5

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
@@ -61,8 +61,10 @@ var TrajectoryEventTypeSchema = z.union([
61
61
  z.literal("reflection"),
62
62
  z.literal("note"),
63
63
  z.literal("error"),
64
+ z.literal("completion-evidence"),
65
+ z.literal("completion-marker"),
64
66
  z.string()
65
- // Allow event types emitted by other tools (e.g. agent-relay's completion-evidence / completion-marker). Downstream code filters to known types.
67
+ // Allow event types emitted by other tools. Downstream code filters to known types.
66
68
  ]);
67
69
  var EventSignificanceSchema = z.enum([
68
70
  "low",
@@ -92,7 +94,7 @@ var DecisionSchema = z.object({
92
94
  });
93
95
  var AgentParticipationSchema = z.object({
94
96
  name: z.string().min(1, "Agent name is required"),
95
- role: z.enum(["lead", "contributor", "reviewer"]),
97
+ role: z.string().min(1, "Agent role is required"),
96
98
  joinedAt: z.string().datetime(),
97
99
  leftAt: z.string().datetime().optional()
98
100
  });
@@ -152,7 +154,7 @@ var TrajectoryTraceRefSchema = z.object({
152
154
  traceId: z.string().optional()
153
155
  });
154
156
  var TrajectorySchema = z.object({
155
- id: z.string().regex(/^traj_[a-z0-9]+$/, "Invalid trajectory ID format"),
157
+ id: z.string().regex(/^traj_[a-z0-9_]+$/, "Invalid trajectory ID format"),
156
158
  version: z.literal(1),
157
159
  task: TaskReferenceSchema,
158
160
  status: TrajectoryStatusSchema,
@@ -161,11 +163,11 @@ var TrajectorySchema = z.object({
161
163
  agents: z.array(AgentParticipationSchema),
162
164
  chapters: z.array(ChapterSchema),
163
165
  retrospective: RetrospectiveSchema.optional(),
164
- commits: z.array(z.string()),
165
- filesChanged: z.array(z.string()),
166
- projectId: z.string(),
166
+ commits: z.array(z.string()).default([]),
167
+ filesChanged: z.array(z.string()).default([]),
168
+ projectId: z.string().optional(),
167
169
  workflowId: z.string().optional(),
168
- tags: z.array(z.string()),
170
+ tags: z.array(z.string()).default([]),
169
171
  _trace: TrajectoryTraceRefSchema.optional()
170
172
  });
171
173
  var CreateTrajectoryInputSchema = z.object({
@@ -567,11 +569,11 @@ function extractDecisions(trajectory) {
567
569
  }
568
570
 
569
571
  // src/storage/file.ts
570
- function expandPath(path) {
571
- if (path.startsWith("~")) {
572
- return join(process.env.HOME ?? "", path.slice(1));
572
+ function expandPath(path2) {
573
+ if (path2.startsWith("~")) {
574
+ return join(process.env.HOME ?? "", path2.slice(1));
573
575
  }
574
- return path;
576
+ return path2;
575
577
  }
576
578
  function getSearchPaths() {
577
579
  const searchPathsEnv = process.env.TRAJECTORIES_SEARCH_PATHS;
@@ -616,11 +618,129 @@ var FileStorage = class {
616
618
  trajectories: {}
617
619
  });
618
620
  }
621
+ await this.reconcileIndex();
622
+ }
623
+ /**
624
+ * Scan active/ and completed/ recursively and add any trajectory files
625
+ * missing from the index. Existing entries are preserved — reconcile
626
+ * only adds, never removes.
627
+ *
628
+ * Handles three on-disk layouts in completed/:
629
+ * - flat: completed/{id}.json (legacy workforce data)
630
+ * - monthly: completed/YYYY-MM/{id}.json (current save() writes)
631
+ * - nested: completed/.../{id}.json (defensive — any depth)
632
+ *
633
+ * Returns a ReconcileSummary so tests and CLI wrappers can observe
634
+ * outcomes without parsing logs. Only writes the index if anything was
635
+ * added.
636
+ */
637
+ async reconcileIndex() {
638
+ const summary = {
639
+ scanned: 0,
640
+ added: 0,
641
+ alreadyIndexed: 0,
642
+ skippedMalformedJson: 0,
643
+ skippedSchemaViolation: 0,
644
+ skippedIoError: 0
645
+ };
646
+ const index = await this.loadIndex();
647
+ const before = Object.keys(index.trajectories).length;
648
+ const discovered = [];
649
+ try {
650
+ const activeFiles = await readdir(this.activeDir);
651
+ for (const file of activeFiles) {
652
+ if (!file.endsWith(".json")) continue;
653
+ discovered.push(join(this.activeDir, file));
654
+ }
655
+ } catch (error) {
656
+ if (error.code !== "ENOENT") throw error;
657
+ }
658
+ await this.walkJsonFilesInto(this.completedDir, discovered);
659
+ for (const filePath of discovered) {
660
+ summary.scanned += 1;
661
+ const result = await this.readTrajectoryFile(filePath);
662
+ if (!result.ok) {
663
+ if (result.reason === "malformed_json") {
664
+ summary.skippedMalformedJson += 1;
665
+ } else if (result.reason === "schema_violation") {
666
+ summary.skippedSchemaViolation += 1;
667
+ } else {
668
+ summary.skippedIoError += 1;
669
+ }
670
+ continue;
671
+ }
672
+ const trajectory = result.trajectory;
673
+ if (index.trajectories[trajectory.id]) {
674
+ summary.alreadyIndexed += 1;
675
+ continue;
676
+ }
677
+ index.trajectories[trajectory.id] = {
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
+ }
689
+ const hadSkips = summary.skippedMalformedJson + summary.skippedSchemaViolation + summary.skippedIoError > 0;
690
+ if (summary.added > 0 || hadSkips) {
691
+ const parts = [`reconciled ${summary.added}/${summary.scanned}`];
692
+ if (summary.skippedMalformedJson > 0) {
693
+ parts.push(`malformed: ${summary.skippedMalformedJson}`);
694
+ }
695
+ if (summary.skippedSchemaViolation > 0) {
696
+ parts.push(`invalid: ${summary.skippedSchemaViolation}`);
697
+ }
698
+ if (summary.skippedIoError > 0) {
699
+ parts.push(`io: ${summary.skippedIoError}`);
700
+ }
701
+ console.warn(`[trajectories] ${parts.join(", ")}`);
702
+ }
703
+ return summary;
704
+ }
705
+ /**
706
+ * Recursively collect all .json file paths under `dir` into `out`.
707
+ * Silently treats a missing directory as empty.
708
+ */
709
+ async walkJsonFilesInto(dir, out) {
710
+ let entries;
711
+ try {
712
+ entries = await readdir(dir, { withFileTypes: true });
713
+ } catch (error) {
714
+ if (error.code === "ENOENT") return;
715
+ throw error;
716
+ }
717
+ for (const entry of entries) {
718
+ const entryPath = join(dir, entry.name);
719
+ if (entry.isDirectory()) {
720
+ await this.walkJsonFilesInto(entryPath, out);
721
+ } else if (entry.isFile() && entry.name.endsWith(".json")) {
722
+ out.push(entryPath);
723
+ }
724
+ }
619
725
  }
620
726
  /**
621
- * Save a trajectory
727
+ * Save a trajectory.
728
+ *
729
+ * Validates the input against the trajectory schema before touching
730
+ * disk. Closes the historical read/write asymmetry where save() would
731
+ * happily write data that the reader then rejected, producing files
732
+ * that could never be loaded back.
622
733
  */
623
- async save(trajectory) {
734
+ async save(input) {
735
+ const validation = validateTrajectory(input);
736
+ if (!validation.success) {
737
+ const issues = validation.errors?.issues.map((issue) => {
738
+ const path2 = issue.path.length > 0 ? issue.path.join(".") : "root";
739
+ return `${path2}: ${issue.message}`;
740
+ }).join("; ") ?? "unknown validation error";
741
+ throw new Error(`Cannot save invalid trajectory: ${issues}`);
742
+ }
743
+ const trajectory = validation.data;
624
744
  const isCompleted = trajectory.status === "completed" || trajectory.status === "abandoned";
625
745
  let filePath;
626
746
  if (isCompleted) {
@@ -650,19 +770,23 @@ var FileStorage = class {
650
770
  async get(id) {
651
771
  const activePath = join(this.activeDir, `${id}.json`);
652
772
  if (existsSync(activePath)) {
653
- return this.readTrajectoryFile(activePath);
773
+ return this.readTrajectoryOrNull(activePath);
654
774
  }
655
775
  const index = await this.loadIndex();
656
776
  const entry = index.trajectories[id];
657
777
  if (entry?.path && existsSync(entry.path)) {
658
- return this.readTrajectoryFile(entry.path);
778
+ return this.readTrajectoryOrNull(entry.path);
659
779
  }
660
780
  try {
781
+ const flatPath = join(this.completedDir, `${id}.json`);
782
+ if (existsSync(flatPath)) {
783
+ return this.readTrajectoryOrNull(flatPath);
784
+ }
661
785
  const months = await readdir(this.completedDir);
662
786
  for (const month of months) {
663
787
  const filePath = join(this.completedDir, month, `${id}.json`);
664
788
  if (existsSync(filePath)) {
665
- return this.readTrajectoryFile(filePath);
789
+ return this.readTrajectoryOrNull(filePath);
666
790
  }
667
791
  }
668
792
  } catch (error) {
@@ -685,7 +809,7 @@ var FileStorage = class {
685
809
  let mostRecent = null;
686
810
  let mostRecentTime = 0;
687
811
  for (const file of jsonFiles) {
688
- const trajectory = await this.readTrajectoryFile(
812
+ const trajectory = await this.readTrajectoryOrNull(
689
813
  join(this.activeDir, file)
690
814
  );
691
815
  if (trajectory) {
@@ -735,7 +859,7 @@ var FileStorage = class {
735
859
  return sortOrder === "asc" ? cmp : -cmp;
736
860
  });
737
861
  const offset = query.offset ?? 0;
738
- const limit = query.limit ?? 50;
862
+ const limit = query.limit ?? 500;
739
863
  entries = entries.slice(offset, offset + limit);
740
864
  return Promise.all(
741
865
  entries.map(async ([id, entry]) => {
@@ -813,20 +937,44 @@ var FileStorage = class {
813
937
  async close() {
814
938
  }
815
939
  // Private helpers
816
- async readTrajectoryFile(path) {
940
+ /**
941
+ * Read a trajectory file and return a tagged result so callers can
942
+ * distinguish missing files, malformed JSON, and schema violations.
943
+ *
944
+ * Does NOT log. Callers choose whether to warn, swallow, or throw.
945
+ */
946
+ async readTrajectoryFile(path2) {
947
+ let content;
817
948
  try {
818
- const content = await readFile(path, "utf-8");
819
- const data = JSON.parse(content);
820
- const validation = validateTrajectory(data);
821
- if (validation.success) {
822
- return validation.data;
823
- }
824
- console.error(`Invalid trajectory at ${path}:`, validation.errors);
825
- return null;
949
+ content = await readFile(path2, "utf-8");
826
950
  } catch (error) {
827
- console.error(`Failed to read trajectory at ${path}:`, error);
828
- return null;
951
+ return { ok: false, reason: "io_error", path: path2, error };
829
952
  }
953
+ let data;
954
+ try {
955
+ data = JSON.parse(content);
956
+ } catch (error) {
957
+ return { ok: false, reason: "malformed_json", path: path2, error };
958
+ }
959
+ const validation = validateTrajectory(data);
960
+ if (validation.success) {
961
+ return { ok: true, trajectory: validation.data };
962
+ }
963
+ return {
964
+ ok: false,
965
+ reason: "schema_violation",
966
+ path: path2,
967
+ error: validation.errors
968
+ };
969
+ }
970
+ /**
971
+ * Convenience wrapper for callers that only care whether they got a
972
+ * trajectory. Returns null for any failure and writes nothing to the
973
+ * console — so nothing leaks into test output or the CLI spinner.
974
+ */
975
+ async readTrajectoryOrNull(path2) {
976
+ const result = await this.readTrajectoryFile(path2);
977
+ return result.ok ? result.trajectory : null;
830
978
  }
831
979
  async loadIndex() {
832
980
  try {
@@ -1662,9 +1810,9 @@ async function resolveCLIProvider() {
1662
1810
  return SUPPORTED_CLIS;
1663
1811
  })();
1664
1812
  for (const cli of clisToTry) {
1665
- const path = await findBinary(cli);
1666
- if (path) {
1667
- return new CLIProvider(cli, path);
1813
+ const path2 = await findBinary(cli);
1814
+ if (path2) {
1815
+ return new CLIProvider(cli, path2);
1668
1816
  }
1669
1817
  }
1670
1818
  return null;
@@ -1672,8 +1820,8 @@ async function resolveCLIProvider() {
1672
1820
  async function findBinary(name) {
1673
1821
  try {
1674
1822
  const { stdout } = await execFileAsync("which", [name]);
1675
- const path = stdout.trim();
1676
- if (path) return path;
1823
+ const path2 = stdout.trim();
1824
+ if (path2) return path2;
1677
1825
  } catch {
1678
1826
  }
1679
1827
  const home = homedir();
@@ -2775,8 +2923,8 @@ function generateTrace(trajectory, startRef) {
2775
2923
  return null;
2776
2924
  }
2777
2925
  const model = detectModel();
2778
- const traceFiles = changedFiles.map(({ path, ranges }) => ({
2779
- path,
2926
+ const traceFiles = changedFiles.map(({ path: path2, ranges }) => ({
2927
+ path: path2,
2780
2928
  conversations: [
2781
2929
  {
2782
2930
  contributor: {
@@ -2923,8 +3071,11 @@ if [ -z "$ACTIVE_FILE" ]; then
2923
3071
  exit 0
2924
3072
  fi
2925
3073
 
2926
- # Extract trajectory ID (grep for the "id" field)
2927
- TRAJ_ID=$(grep -o '"id"[[:space:]]*:[[:space:]]*"traj_[a-z0-9]*"' "$ACTIVE_FILE" | head -1 | grep -o 'traj_[a-z0-9]*')
3074
+ # Extract trajectory ID (grep for the "id" field). Character class must
3075
+ # include underscore to match legacy traj_<timestamp>_<hex> ids -- without
3076
+ # it, grep -o silently truncates at the first internal underscore and
3077
+ # emits a wrong (shorter) id into the commit trailer.
3078
+ TRAJ_ID=$(grep -o '"id"[[:space:]]*:[[:space:]]*"traj_[a-z0-9_]*"' "$ACTIVE_FILE" | head -1 | grep -o 'traj_[a-z0-9_]*')
2928
3079
  if [ -z "$TRAJ_ID" ]; then
2929
3080
  exit 0
2930
3081
  fi
@@ -3882,19 +4033,19 @@ function registerExportCommand(program2) {
3882
4033
  }
3883
4034
  });
3884
4035
  }
3885
- function openInBrowser(path) {
4036
+ function openInBrowser(path2) {
3886
4037
  const platform = process.platform;
3887
4038
  let command;
3888
4039
  if (platform === "darwin") {
3889
- command = `open "${path}"`;
4040
+ command = `open "${path2}"`;
3890
4041
  } else if (platform === "win32") {
3891
- command = `start "" "${path}"`;
4042
+ command = `start "" "${path2}"`;
3892
4043
  } else {
3893
- command = `xdg-open "${path}"`;
4044
+ command = `xdg-open "${path2}"`;
3894
4045
  }
3895
4046
  exec(command, (error) => {
3896
4047
  if (error) {
3897
- console.log(`Open manually: file://${path}`);
4048
+ console.log(`Open manually: file://${path2}`);
3898
4049
  }
3899
4050
  });
3900
4051
  }
@@ -4399,8 +4550,37 @@ function registerCommands(program2) {
4399
4550
  registerCompactCommand(program2);
4400
4551
  }
4401
4552
 
4553
+ // src/cli/version.ts
4554
+ import fs from "fs";
4555
+ import path from "path";
4556
+ import { fileURLToPath } from "url";
4557
+ function findPackageJson(startDir) {
4558
+ let dir = startDir;
4559
+ while (dir !== path.dirname(dir)) {
4560
+ const candidate = path.join(dir, "package.json");
4561
+ if (fs.existsSync(candidate)) {
4562
+ return candidate;
4563
+ }
4564
+ dir = path.dirname(dir);
4565
+ }
4566
+ throw new Error("Could not find package.json");
4567
+ }
4568
+ function resolveCliVersion() {
4569
+ try {
4570
+ const here = path.dirname(fileURLToPath(import.meta.url));
4571
+ const packageJsonPath = findPackageJson(here);
4572
+ const packageJson = JSON.parse(
4573
+ fs.readFileSync(packageJsonPath, "utf-8")
4574
+ );
4575
+ return packageJson.version ?? "unknown";
4576
+ } catch {
4577
+ return "unknown";
4578
+ }
4579
+ }
4580
+ var VERSION = resolveCliVersion();
4581
+
4402
4582
  // src/cli/index.ts
4403
- program.name("trail").description("Leave a trail of your work for others to follow").version("0.1.0").option(
4583
+ program.name("trail").description("Leave a trail of your work for others to follow").version(VERSION).option(
4404
4584
  "--data-dir <path>",
4405
4585
  "Override trajectory storage directory (or set TRAJECTORIES_DATA_DIR)"
4406
4586
  ).hook("preAction", (thisCommand) => {