agent-relay 4.0.19 → 4.0.21

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.
Files changed (130) hide show
  1. package/dist/index.cjs +1407 -537
  2. package/dist/src/cli/commands/messaging.d.ts +44 -0
  3. package/dist/src/cli/commands/messaging.d.ts.map +1 -1
  4. package/dist/src/cli/commands/messaging.js +195 -8
  5. package/dist/src/cli/commands/messaging.js.map +1 -1
  6. package/dist/src/cli/commands/setup.d.ts +30 -1
  7. package/dist/src/cli/commands/setup.d.ts.map +1 -1
  8. package/dist/src/cli/commands/setup.js +102 -85
  9. package/dist/src/cli/commands/setup.js.map +1 -1
  10. package/node_modules/@agent-relay/cloud/dist/auth.d.ts +2 -2
  11. package/node_modules/@agent-relay/cloud/dist/auth.d.ts.map +1 -1
  12. package/node_modules/@agent-relay/cloud/dist/auth.js +108 -62
  13. package/node_modules/@agent-relay/cloud/dist/auth.js.map +1 -1
  14. package/node_modules/@agent-relay/cloud/package.json +2 -2
  15. package/node_modules/@agent-relay/config/package.json +1 -1
  16. package/node_modules/@agent-relay/hooks/package.json +4 -4
  17. package/node_modules/@agent-relay/sdk/dist/workflows/trajectory.d.ts +5 -35
  18. package/node_modules/@agent-relay/sdk/dist/workflows/trajectory.d.ts.map +1 -1
  19. package/node_modules/@agent-relay/sdk/dist/workflows/trajectory.js +158 -292
  20. package/node_modules/@agent-relay/sdk/dist/workflows/trajectory.js.map +1 -1
  21. package/node_modules/@agent-relay/sdk/package.json +3 -2
  22. package/node_modules/@agent-relay/telemetry/package.json +1 -1
  23. package/node_modules/@agent-relay/trajectory/package.json +2 -2
  24. package/node_modules/@agent-relay/user-directory/package.json +2 -2
  25. package/node_modules/@agent-relay/utils/package.json +2 -2
  26. package/node_modules/@clack/core/CHANGELOG.md +200 -0
  27. package/node_modules/@clack/core/LICENSE +9 -0
  28. package/node_modules/@clack/core/README.md +22 -0
  29. package/node_modules/@clack/core/dist/index.cjs +15 -0
  30. package/node_modules/@clack/core/dist/index.cjs.map +1 -0
  31. package/node_modules/@clack/core/dist/index.d.cts +151 -0
  32. package/node_modules/@clack/core/dist/index.d.mts +151 -0
  33. package/node_modules/@clack/core/dist/index.d.ts +151 -0
  34. package/node_modules/@clack/core/dist/index.mjs +15 -0
  35. package/node_modules/@clack/core/dist/index.mjs.map +1 -0
  36. package/node_modules/@clack/core/package.json +62 -0
  37. package/node_modules/@clack/prompts/CHANGELOG.md +256 -0
  38. package/node_modules/@clack/prompts/LICENSE +23 -0
  39. package/node_modules/@clack/prompts/README.md +158 -0
  40. package/node_modules/@clack/prompts/dist/index.cjs +77 -0
  41. package/node_modules/@clack/prompts/dist/index.d.ts +106 -0
  42. package/node_modules/@clack/prompts/dist/index.mjs +77 -0
  43. package/node_modules/@clack/prompts/node_modules/is-unicode-supported/index.d.ts +12 -0
  44. package/node_modules/@clack/prompts/node_modules/is-unicode-supported/index.js +17 -0
  45. package/node_modules/@clack/prompts/node_modules/is-unicode-supported/license +9 -0
  46. package/node_modules/@clack/prompts/node_modules/is-unicode-supported/package.json +43 -0
  47. package/node_modules/@clack/prompts/node_modules/is-unicode-supported/readme.md +35 -0
  48. package/node_modules/@clack/prompts/package.json +65 -0
  49. package/node_modules/@smithy/config-resolver/package.json +2 -2
  50. package/node_modules/@smithy/util-defaults-mode-node/package.json +2 -2
  51. package/node_modules/@smithy/util-endpoints/dist-cjs/index.js +154 -61
  52. package/node_modules/@smithy/util-endpoints/dist-es/bdd/BinaryDecisionDiagram.js +15 -0
  53. package/node_modules/@smithy/util-endpoints/dist-es/decideEndpoint.js +41 -0
  54. package/node_modules/@smithy/util-endpoints/dist-es/index.js +2 -0
  55. package/node_modules/@smithy/util-endpoints/dist-es/lib/coalesce.js +8 -0
  56. package/node_modules/@smithy/util-endpoints/dist-es/lib/index.js +3 -0
  57. package/node_modules/@smithy/util-endpoints/dist-es/lib/ite.js +3 -0
  58. package/node_modules/@smithy/util-endpoints/dist-es/lib/split.js +13 -0
  59. package/node_modules/@smithy/util-endpoints/dist-es/lib/substring.js +1 -1
  60. package/node_modules/@smithy/util-endpoints/dist-es/utils/endpointFunctions.js +4 -1
  61. package/node_modules/@smithy/util-endpoints/dist-es/utils/evaluateExpression.js +20 -5
  62. package/node_modules/@smithy/util-endpoints/dist-es/utils/evaluateTemplate.js +3 -6
  63. package/node_modules/@smithy/util-endpoints/dist-es/utils/getReferenceValue.js +1 -5
  64. package/node_modules/@smithy/util-endpoints/dist-types/bdd/BinaryDecisionDiagram.d.ts +22 -0
  65. package/node_modules/@smithy/util-endpoints/dist-types/decideEndpoint.d.ts +7 -0
  66. package/node_modules/@smithy/util-endpoints/dist-types/index.d.ts +2 -0
  67. package/node_modules/@smithy/util-endpoints/dist-types/lib/coalesce.d.ts +7 -0
  68. package/node_modules/@smithy/util-endpoints/dist-types/lib/index.d.ts +3 -0
  69. package/node_modules/@smithy/util-endpoints/dist-types/lib/ite.d.ts +6 -0
  70. package/node_modules/@smithy/util-endpoints/dist-types/lib/split.d.ts +11 -0
  71. package/node_modules/@smithy/util-endpoints/dist-types/utils/endpointFunctions.d.ts +4 -0
  72. package/node_modules/@smithy/util-endpoints/dist-types/utils/getReferenceValue.d.ts +3 -1
  73. package/node_modules/@smithy/util-endpoints/package.json +1 -1
  74. package/node_modules/agent-trajectories/README.md +562 -0
  75. package/node_modules/agent-trajectories/dist/chunk-W222QB6V.js +2036 -0
  76. package/node_modules/agent-trajectories/dist/chunk-W222QB6V.js.map +1 -0
  77. package/node_modules/agent-trajectories/dist/cli/index.d.ts +2 -0
  78. package/node_modules/agent-trajectories/dist/cli/index.js +4592 -0
  79. package/node_modules/agent-trajectories/dist/cli/index.js.map +1 -0
  80. package/node_modules/agent-trajectories/dist/index-7tzw_CMS.d.ts +1777 -0
  81. package/node_modules/agent-trajectories/dist/index.d.ts +90 -0
  82. package/node_modules/agent-trajectories/dist/index.js +73 -0
  83. package/node_modules/agent-trajectories/dist/index.js.map +1 -0
  84. package/node_modules/agent-trajectories/dist/sdk/index.d.ts +2 -0
  85. package/node_modules/agent-trajectories/dist/sdk/index.js +39 -0
  86. package/node_modules/agent-trajectories/dist/sdk/index.js.map +1 -0
  87. package/node_modules/agent-trajectories/package.json +72 -0
  88. package/node_modules/commander/LICENSE +22 -0
  89. package/node_modules/commander/Readme.md +1157 -0
  90. package/node_modules/commander/esm.mjs +16 -0
  91. package/node_modules/commander/index.js +24 -0
  92. package/node_modules/commander/lib/argument.js +149 -0
  93. package/node_modules/commander/lib/command.js +2509 -0
  94. package/node_modules/commander/lib/error.js +39 -0
  95. package/node_modules/commander/lib/help.js +520 -0
  96. package/node_modules/commander/lib/option.js +330 -0
  97. package/node_modules/commander/lib/suggestSimilar.js +101 -0
  98. package/node_modules/commander/package-support.json +16 -0
  99. package/node_modules/commander/package.json +84 -0
  100. package/node_modules/commander/typings/esm.d.mts +3 -0
  101. package/node_modules/commander/typings/index.d.ts +969 -0
  102. package/node_modules/picocolors/LICENSE +15 -0
  103. package/node_modules/picocolors/README.md +21 -0
  104. package/node_modules/picocolors/package.json +25 -0
  105. package/node_modules/picocolors/picocolors.browser.js +4 -0
  106. package/node_modules/picocolors/picocolors.d.ts +5 -0
  107. package/node_modules/picocolors/picocolors.js +75 -0
  108. package/node_modules/picocolors/types.d.ts +51 -0
  109. package/node_modules/sisteransi/license +21 -0
  110. package/node_modules/sisteransi/package.json +34 -0
  111. package/node_modules/sisteransi/readme.md +113 -0
  112. package/node_modules/sisteransi/src/index.js +58 -0
  113. package/node_modules/sisteransi/src/sisteransi.d.ts +35 -0
  114. package/package.json +10 -10
  115. package/packages/cloud/dist/auth.d.ts +2 -2
  116. package/packages/cloud/dist/auth.d.ts.map +1 -1
  117. package/packages/cloud/dist/auth.js +108 -62
  118. package/packages/cloud/dist/auth.js.map +1 -1
  119. package/packages/cloud/package.json +2 -2
  120. package/packages/config/package.json +1 -1
  121. package/packages/hooks/package.json +4 -4
  122. package/packages/sdk/dist/workflows/trajectory.d.ts +5 -35
  123. package/packages/sdk/dist/workflows/trajectory.d.ts.map +1 -1
  124. package/packages/sdk/dist/workflows/trajectory.js +158 -292
  125. package/packages/sdk/dist/workflows/trajectory.js.map +1 -1
  126. package/packages/sdk/package.json +3 -2
  127. package/packages/telemetry/package.json +1 -1
  128. package/packages/trajectory/package.json +2 -2
  129. package/packages/user-directory/package.json +2 -2
  130. package/packages/utils/package.json +2 -2
@@ -0,0 +1,4592 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { program } from "commander";
5
+
6
+ // src/core/id.ts
7
+ import { webcrypto } from "crypto";
8
+ var ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
9
+ var ID_LENGTH = 12;
10
+ function generateRandomId(length = ID_LENGTH) {
11
+ let id = "";
12
+ const randomValues = new Uint8Array(length);
13
+ webcrypto.getRandomValues(randomValues);
14
+ for (let i = 0; i < length; i++) {
15
+ id += ALPHABET[randomValues[i] % ALPHABET.length];
16
+ }
17
+ return id;
18
+ }
19
+ function generateTrajectoryId() {
20
+ return `traj_${generateRandomId()}`;
21
+ }
22
+ function generateChapterId() {
23
+ return `chap_${generateRandomId()}`;
24
+ }
25
+
26
+ // src/core/schema.ts
27
+ import { z } from "zod";
28
+ var TaskSourceSystemSchema = z.union([
29
+ z.literal("beads"),
30
+ z.literal("github"),
31
+ z.literal("linear"),
32
+ z.literal("jira"),
33
+ z.literal("plain"),
34
+ z.string()
35
+ // Allow custom systems
36
+ ]);
37
+ var TaskSourceSchema = z.object({
38
+ system: TaskSourceSystemSchema,
39
+ id: z.string().min(1, "Task ID is required"),
40
+ url: z.string().url().optional()
41
+ });
42
+ var TaskReferenceSchema = z.object({
43
+ title: z.string().min(1, "Trajectory title is required").max(500, "Trajectory title must be 500 characters or less"),
44
+ description: z.string().optional(),
45
+ source: TaskSourceSchema.optional()
46
+ });
47
+ var TrajectoryStatusSchema = z.enum([
48
+ "active",
49
+ "completed",
50
+ "abandoned"
51
+ ]);
52
+ var TrajectoryEventTypeSchema = z.union([
53
+ z.literal("prompt"),
54
+ z.literal("thinking"),
55
+ z.literal("tool_call"),
56
+ z.literal("tool_result"),
57
+ z.literal("message_sent"),
58
+ z.literal("message_received"),
59
+ z.literal("decision"),
60
+ z.literal("finding"),
61
+ z.literal("reflection"),
62
+ z.literal("note"),
63
+ z.literal("error"),
64
+ 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.
66
+ ]);
67
+ var EventSignificanceSchema = z.enum([
68
+ "low",
69
+ "medium",
70
+ "high",
71
+ "critical"
72
+ ]);
73
+ var TrajectoryEventSchema = z.object({
74
+ ts: z.number().int().positive(),
75
+ type: TrajectoryEventTypeSchema,
76
+ content: z.string().min(1, "Event content is required"),
77
+ raw: z.unknown().optional(),
78
+ significance: EventSignificanceSchema.optional(),
79
+ tags: z.array(z.string()).optional(),
80
+ confidence: z.number().min(0, "Confidence must be between 0 and 1").max(1, "Confidence must be between 0 and 1").optional()
81
+ });
82
+ var AlternativeSchema = z.object({
83
+ option: z.string().min(1, "Alternative option is required"),
84
+ reason: z.string().optional()
85
+ });
86
+ var DecisionSchema = z.object({
87
+ question: z.string().min(1, "Decision question is required"),
88
+ chosen: z.string().min(1, "Chosen option is required"),
89
+ alternatives: z.array(z.union([z.string(), AlternativeSchema])),
90
+ reasoning: z.string().min(1, "Decision reasoning is required"),
91
+ confidence: z.number().min(0, "Confidence must be between 0 and 1").max(1, "Confidence must be between 0 and 1").optional()
92
+ });
93
+ var AgentParticipationSchema = z.object({
94
+ name: z.string().min(1, "Agent name is required"),
95
+ role: z.string().min(1, "Agent role is required"),
96
+ joinedAt: z.string().datetime(),
97
+ leftAt: z.string().datetime().optional()
98
+ });
99
+ var ChapterSchema = z.object({
100
+ id: z.string().min(1),
101
+ title: z.string().min(1, "Chapter title is required"),
102
+ agentName: z.string().min(1, "Agent name is required"),
103
+ startedAt: z.string().datetime(),
104
+ endedAt: z.string().datetime().optional(),
105
+ events: z.array(TrajectoryEventSchema)
106
+ });
107
+ var RetrospectiveSchema = z.object({
108
+ summary: z.string().min(1, "Retrospective summary is required"),
109
+ approach: z.string().min(1, "Approach description is required"),
110
+ decisions: z.array(DecisionSchema).optional(),
111
+ challenges: z.array(z.string()).optional(),
112
+ learnings: z.array(z.string()).optional(),
113
+ suggestions: z.array(z.string()).optional(),
114
+ confidence: z.number().min(0, "Confidence must be between 0 and 1").max(1, "Confidence must be between 0 and 1"),
115
+ timeSpent: z.string().optional()
116
+ });
117
+ var TraceRangeSchema = z.object({
118
+ start_line: z.number().int().positive("Start line must be positive"),
119
+ end_line: z.number().int().positive("End line must be positive"),
120
+ revision: z.string().optional(),
121
+ content_hash: z.string().optional()
122
+ });
123
+ var ContributorTypeSchema = z.enum([
124
+ "human",
125
+ "ai",
126
+ "mixed",
127
+ "unknown"
128
+ ]);
129
+ var TraceContributorSchema = z.object({
130
+ type: ContributorTypeSchema,
131
+ model_id: z.string().max(250).optional()
132
+ });
133
+ var TraceConversationSchema = z.object({
134
+ contributor: TraceContributorSchema,
135
+ url: z.string().url().optional(),
136
+ ranges: z.array(TraceRangeSchema)
137
+ });
138
+ var TraceFileSchema = z.object({
139
+ path: z.string().min(1, "File path is required"),
140
+ conversations: z.array(TraceConversationSchema)
141
+ });
142
+ var TraceRecordSchema = z.object({
143
+ version: z.string().min(1, "Version is required"),
144
+ id: z.string().min(1, "Trace ID is required"),
145
+ timestamp: z.string().datetime(),
146
+ trajectory: z.string().optional(),
147
+ files: z.array(TraceFileSchema)
148
+ });
149
+ var TrajectoryTraceRefSchema = z.object({
150
+ startRef: z.string().min(1, "Start ref is required"),
151
+ endRef: z.string().optional(),
152
+ traceId: z.string().optional()
153
+ });
154
+ var TrajectorySchema = z.object({
155
+ id: z.string().regex(/^traj_[a-z0-9_]+$/, "Invalid trajectory ID format"),
156
+ version: z.literal(1),
157
+ task: TaskReferenceSchema,
158
+ status: TrajectoryStatusSchema,
159
+ startedAt: z.string().datetime(),
160
+ completedAt: z.string().datetime().optional(),
161
+ agents: z.array(AgentParticipationSchema),
162
+ chapters: z.array(ChapterSchema),
163
+ retrospective: RetrospectiveSchema.optional(),
164
+ commits: z.array(z.string()).default([]),
165
+ filesChanged: z.array(z.string()).default([]),
166
+ projectId: z.string().optional(),
167
+ workflowId: z.string().optional(),
168
+ tags: z.array(z.string()).default([]),
169
+ _trace: TrajectoryTraceRefSchema.optional()
170
+ });
171
+ var CreateTrajectoryInputSchema = z.object({
172
+ title: z.string().min(1, "Trajectory title is required").max(500, "Trajectory title must be 500 characters or less"),
173
+ description: z.string().optional(),
174
+ source: TaskSourceSchema.optional(),
175
+ projectId: z.string().optional(),
176
+ tags: z.array(z.string()).optional()
177
+ });
178
+ var AddChapterInputSchema = z.object({
179
+ title: z.string().min(1, "Chapter title is required"),
180
+ agentName: z.string().min(1, "Agent name is required")
181
+ });
182
+ var AddEventInputSchema = z.object({
183
+ type: TrajectoryEventTypeSchema,
184
+ content: z.string().min(1, "Event content is required"),
185
+ raw: z.unknown().optional(),
186
+ significance: EventSignificanceSchema.optional(),
187
+ tags: z.array(z.string()).optional()
188
+ });
189
+ var CompleteTrajectoryInputSchema = z.object({
190
+ summary: z.string().min(1, "Retrospective summary is required"),
191
+ approach: z.string().min(1, "Approach description is required"),
192
+ decisions: z.array(DecisionSchema).optional(),
193
+ challenges: z.array(z.string()).optional(),
194
+ learnings: z.array(z.string()).optional(),
195
+ suggestions: z.array(z.string()).optional(),
196
+ confidence: z.number().min(0, "Confidence must be between 0 and 1").max(1, "Confidence must be between 0 and 1")
197
+ });
198
+ var TrajectoryQuerySchema = z.object({
199
+ status: TrajectoryStatusSchema.optional(),
200
+ since: z.string().datetime().optional(),
201
+ until: z.string().datetime().optional(),
202
+ limit: z.number().int().positive().max(100).optional(),
203
+ offset: z.number().int().nonnegative().optional(),
204
+ sortBy: z.enum(["startedAt", "completedAt", "title"]).optional(),
205
+ sortOrder: z.enum(["asc", "desc"]).optional()
206
+ });
207
+ function validateTrajectory(data) {
208
+ const result = TrajectorySchema.safeParse(data);
209
+ if (result.success) {
210
+ return { success: true, data: result.data };
211
+ }
212
+ return { success: false, errors: result.error };
213
+ }
214
+
215
+ // src/core/trajectory.ts
216
+ var TrajectoryError = class extends Error {
217
+ constructor(message, code, suggestion) {
218
+ super(message);
219
+ this.code = code;
220
+ this.suggestion = suggestion;
221
+ this.name = "TrajectoryError";
222
+ }
223
+ };
224
+ function createTrajectory(input) {
225
+ const validation = CreateTrajectoryInputSchema.safeParse(input);
226
+ if (!validation.success) {
227
+ const firstError = validation.error.errors[0];
228
+ throw new TrajectoryError(
229
+ firstError.message,
230
+ "VALIDATION_ERROR",
231
+ "Check your input and try again"
232
+ );
233
+ }
234
+ const now = (/* @__PURE__ */ new Date()).toISOString();
235
+ return {
236
+ id: generateTrajectoryId(),
237
+ version: 1,
238
+ task: {
239
+ title: input.title,
240
+ description: input.description,
241
+ source: input.source
242
+ },
243
+ status: "active",
244
+ startedAt: now,
245
+ agents: [],
246
+ chapters: [],
247
+ commits: [],
248
+ filesChanged: [],
249
+ projectId: input.projectId ?? process.cwd(),
250
+ tags: input.tags ?? []
251
+ };
252
+ }
253
+ function addChapter(trajectory, input) {
254
+ if (trajectory.status === "completed") {
255
+ throw new TrajectoryError(
256
+ "Cannot add chapter to completed trajectory",
257
+ "TRAJECTORY_ALREADY_COMPLETED",
258
+ "Start a new trajectory instead"
259
+ );
260
+ }
261
+ const now = (/* @__PURE__ */ new Date()).toISOString();
262
+ const updatedChapters = trajectory.chapters.map((chapter, index) => {
263
+ if (index === trajectory.chapters.length - 1 && !chapter.endedAt) {
264
+ return { ...chapter, endedAt: now };
265
+ }
266
+ return chapter;
267
+ });
268
+ const newChapter = {
269
+ id: generateChapterId(),
270
+ title: input.title,
271
+ agentName: input.agentName,
272
+ startedAt: now,
273
+ events: []
274
+ };
275
+ let updatedAgents = trajectory.agents;
276
+ const agentExists = trajectory.agents.some((a) => a.name === input.agentName);
277
+ if (!agentExists) {
278
+ const isFirstAgent = trajectory.agents.length === 0;
279
+ updatedAgents = [
280
+ ...trajectory.agents,
281
+ {
282
+ name: input.agentName,
283
+ role: isFirstAgent ? "lead" : "contributor",
284
+ joinedAt: now
285
+ }
286
+ ];
287
+ }
288
+ return {
289
+ ...trajectory,
290
+ agents: updatedAgents,
291
+ chapters: [...updatedChapters, newChapter]
292
+ };
293
+ }
294
+ function addEvent(trajectory, input) {
295
+ let updatedTrajectory = trajectory;
296
+ if (trajectory.chapters.length === 0) {
297
+ updatedTrajectory = addChapter(trajectory, {
298
+ title: "Work",
299
+ agentName: "default"
300
+ });
301
+ }
302
+ const event = {
303
+ ts: Date.now(),
304
+ type: input.type,
305
+ content: input.content,
306
+ raw: input.raw,
307
+ significance: input.significance,
308
+ tags: input.tags
309
+ };
310
+ const chapters = [...updatedTrajectory.chapters];
311
+ const lastChapter = chapters[chapters.length - 1];
312
+ chapters[chapters.length - 1] = {
313
+ ...lastChapter,
314
+ events: [...lastChapter.events, event]
315
+ };
316
+ return {
317
+ ...updatedTrajectory,
318
+ chapters
319
+ };
320
+ }
321
+ function addDecision(trajectory, decision) {
322
+ return addEvent(trajectory, {
323
+ type: "decision",
324
+ content: `${decision.question}: ${decision.chosen}`,
325
+ raw: decision,
326
+ significance: "high"
327
+ });
328
+ }
329
+ function completeTrajectory(trajectory, input) {
330
+ if (trajectory.status === "completed") {
331
+ throw new TrajectoryError(
332
+ "Trajectory is already completed",
333
+ "TRAJECTORY_ALREADY_COMPLETED",
334
+ "Start a new trajectory instead"
335
+ );
336
+ }
337
+ const validation = CompleteTrajectoryInputSchema.safeParse(input);
338
+ if (!validation.success) {
339
+ const firstError = validation.error.errors[0];
340
+ throw new TrajectoryError(
341
+ firstError.message,
342
+ "VALIDATION_ERROR",
343
+ "Check your input and try again"
344
+ );
345
+ }
346
+ const now = (/* @__PURE__ */ new Date()).toISOString();
347
+ const chapters = trajectory.chapters.map((chapter, index) => {
348
+ if (index === trajectory.chapters.length - 1 && !chapter.endedAt) {
349
+ return { ...chapter, endedAt: now };
350
+ }
351
+ return chapter;
352
+ });
353
+ return {
354
+ ...trajectory,
355
+ status: "completed",
356
+ completedAt: now,
357
+ chapters,
358
+ retrospective: {
359
+ summary: input.summary,
360
+ approach: input.approach,
361
+ decisions: input.decisions,
362
+ challenges: input.challenges,
363
+ learnings: input.learnings,
364
+ suggestions: input.suggestions,
365
+ confidence: input.confidence
366
+ }
367
+ };
368
+ }
369
+ function abandonTrajectory(trajectory, reason) {
370
+ const now = (/* @__PURE__ */ new Date()).toISOString();
371
+ const chapters = trajectory.chapters.map((chapter, index) => {
372
+ if (index === trajectory.chapters.length - 1 && !chapter.endedAt) {
373
+ return { ...chapter, endedAt: now };
374
+ }
375
+ return chapter;
376
+ });
377
+ let updatedChapters = chapters;
378
+ if (reason && chapters.length > 0) {
379
+ const lastChapter = chapters[chapters.length - 1];
380
+ updatedChapters = [
381
+ ...chapters.slice(0, -1),
382
+ {
383
+ ...lastChapter,
384
+ events: [
385
+ ...lastChapter.events,
386
+ {
387
+ ts: Date.now(),
388
+ type: "note",
389
+ content: `Abandoned: ${reason}`,
390
+ significance: "high"
391
+ }
392
+ ]
393
+ }
394
+ ];
395
+ }
396
+ return {
397
+ ...trajectory,
398
+ status: "abandoned",
399
+ completedAt: now,
400
+ chapters: updatedChapters
401
+ };
402
+ }
403
+
404
+ // src/storage/file.ts
405
+ import { existsSync } from "fs";
406
+ import { mkdir, readFile, readdir, unlink, writeFile } from "fs/promises";
407
+ import { join } from "path";
408
+
409
+ // src/export/markdown.ts
410
+ function exportToMarkdown(trajectory) {
411
+ const lines = [];
412
+ lines.push(`# Trajectory: ${trajectory.task.title}`);
413
+ lines.push("");
414
+ lines.push(`> **Status:** ${formatStatus(trajectory.status)}`);
415
+ if (trajectory.task.source) {
416
+ const linkText = trajectory.task.source.url ? `[${trajectory.task.source.id}](${trajectory.task.source.url})` : trajectory.task.source.id;
417
+ lines.push(`> **Task:** ${linkText}`);
418
+ }
419
+ if (trajectory.retrospective?.confidence !== void 0) {
420
+ lines.push(
421
+ `> **Confidence:** ${Math.round(trajectory.retrospective.confidence * 100)}%`
422
+ );
423
+ }
424
+ lines.push(`> **Started:** ${formatDate(trajectory.startedAt)}`);
425
+ if (trajectory.completedAt) {
426
+ lines.push(`> **Completed:** ${formatDate(trajectory.completedAt)}`);
427
+ }
428
+ lines.push("");
429
+ if (trajectory.retrospective) {
430
+ lines.push("---");
431
+ lines.push("");
432
+ lines.push("## Summary");
433
+ lines.push("");
434
+ lines.push(trajectory.retrospective.summary);
435
+ lines.push("");
436
+ if (trajectory.retrospective.approach) {
437
+ lines.push(`**Approach:** ${trajectory.retrospective.approach}`);
438
+ lines.push("");
439
+ }
440
+ }
441
+ const decisions = extractDecisions(trajectory);
442
+ if (decisions.length > 0) {
443
+ lines.push("---");
444
+ lines.push("");
445
+ lines.push("## Key Decisions");
446
+ lines.push("");
447
+ for (const decision of decisions) {
448
+ lines.push(`### ${decision.question}`);
449
+ lines.push(`- **Chose:** ${decision.chosen}`);
450
+ if (decision.alternatives.length > 0) {
451
+ const altStrings = decision.alternatives.map(
452
+ (a) => typeof a === "string" ? a : a.option
453
+ );
454
+ lines.push(`- **Rejected:** ${altStrings.join(", ")}`);
455
+ }
456
+ lines.push(`- **Reasoning:** ${decision.reasoning}`);
457
+ lines.push("");
458
+ }
459
+ }
460
+ if (trajectory.chapters.length > 0) {
461
+ lines.push("---");
462
+ lines.push("");
463
+ lines.push("## Chapters");
464
+ lines.push("");
465
+ trajectory.chapters.forEach((chapter, index) => {
466
+ lines.push(`### ${index + 1}. ${chapter.title}`);
467
+ lines.push(`*Agent: ${chapter.agentName}*`);
468
+ lines.push("");
469
+ if (chapter.events.length > 0) {
470
+ const significantEvents = chapter.events.filter(
471
+ (e) => e.significance === "high" || e.significance === "critical" || e.type === "decision"
472
+ );
473
+ if (significantEvents.length > 0) {
474
+ for (const event of significantEvents) {
475
+ lines.push(`- ${event.content}`);
476
+ }
477
+ lines.push("");
478
+ }
479
+ }
480
+ });
481
+ }
482
+ if (trajectory.retrospective?.challenges && trajectory.retrospective.challenges.length > 0) {
483
+ lines.push("---");
484
+ lines.push("");
485
+ lines.push("## Challenges");
486
+ lines.push("");
487
+ for (const challenge of trajectory.retrospective.challenges) {
488
+ lines.push(`- ${challenge}`);
489
+ }
490
+ lines.push("");
491
+ }
492
+ if (trajectory.retrospective?.learnings && trajectory.retrospective.learnings.length > 0) {
493
+ lines.push("---");
494
+ lines.push("");
495
+ lines.push("## Learnings");
496
+ lines.push("");
497
+ for (const learning of trajectory.retrospective.learnings) {
498
+ lines.push(`- ${learning}`);
499
+ }
500
+ lines.push("");
501
+ }
502
+ if (trajectory.retrospective?.suggestions && trajectory.retrospective.suggestions.length > 0) {
503
+ lines.push("---");
504
+ lines.push("");
505
+ lines.push("## Suggestions");
506
+ lines.push("");
507
+ for (const suggestion of trajectory.retrospective.suggestions) {
508
+ lines.push(`- ${suggestion}`);
509
+ }
510
+ lines.push("");
511
+ }
512
+ if (trajectory.commits.length > 0 || trajectory.filesChanged.length > 0) {
513
+ lines.push("---");
514
+ lines.push("");
515
+ lines.push("## Artifacts");
516
+ lines.push("");
517
+ if (trajectory.commits.length > 0) {
518
+ lines.push(`**Commits:** ${trajectory.commits.join(", ")}`);
519
+ }
520
+ if (trajectory.filesChanged.length > 0) {
521
+ lines.push(`**Files changed:** ${trajectory.filesChanged.length}`);
522
+ }
523
+ lines.push("");
524
+ }
525
+ return lines.join("\n");
526
+ }
527
+ function formatStatus(status) {
528
+ switch (status) {
529
+ case "active":
530
+ return "\u{1F504} Active";
531
+ case "completed":
532
+ return "\u2705 Completed";
533
+ case "abandoned":
534
+ return "\u274C Abandoned";
535
+ default:
536
+ return status;
537
+ }
538
+ }
539
+ function formatDate(isoString) {
540
+ const date = new Date(isoString);
541
+ return date.toLocaleDateString("en-US", {
542
+ year: "numeric",
543
+ month: "long",
544
+ day: "numeric",
545
+ hour: "2-digit",
546
+ minute: "2-digit"
547
+ });
548
+ }
549
+ function extractDecisions(trajectory) {
550
+ const decisions = [];
551
+ if (trajectory.retrospective?.decisions) {
552
+ decisions.push(...trajectory.retrospective.decisions);
553
+ }
554
+ for (const chapter of trajectory.chapters) {
555
+ for (const event of chapter.events) {
556
+ if (event.type === "decision" && event.raw) {
557
+ const raw = event.raw;
558
+ if (raw.question && raw.chosen && raw.reasoning) {
559
+ if (!decisions.some((d) => d.question === raw.question)) {
560
+ decisions.push(raw);
561
+ }
562
+ }
563
+ }
564
+ }
565
+ }
566
+ return decisions;
567
+ }
568
+
569
+ // src/storage/file.ts
570
+ function expandPath(path2) {
571
+ if (path2.startsWith("~")) {
572
+ return join(process.env.HOME ?? "", path2.slice(1));
573
+ }
574
+ return path2;
575
+ }
576
+ function getSearchPaths() {
577
+ const searchPathsEnv = process.env.TRAJECTORIES_SEARCH_PATHS;
578
+ if (searchPathsEnv) {
579
+ return searchPathsEnv.split(":").map((p) => p.trim()).filter(Boolean).map(expandPath);
580
+ }
581
+ const dataDir = process.env.TRAJECTORIES_DATA_DIR;
582
+ if (dataDir) {
583
+ return [expandPath(dataDir)];
584
+ }
585
+ return [join(process.cwd(), ".trajectories")];
586
+ }
587
+ var FileStorage = class {
588
+ baseDir;
589
+ trajectoriesDir;
590
+ activeDir;
591
+ completedDir;
592
+ indexPath;
593
+ constructor(baseDir) {
594
+ this.baseDir = baseDir ?? process.cwd();
595
+ const dataDir = process.env.TRAJECTORIES_DATA_DIR;
596
+ if (dataDir) {
597
+ this.trajectoriesDir = expandPath(dataDir);
598
+ } else {
599
+ this.trajectoriesDir = join(this.baseDir, ".trajectories");
600
+ }
601
+ this.activeDir = join(this.trajectoriesDir, "active");
602
+ this.completedDir = join(this.trajectoriesDir, "completed");
603
+ this.indexPath = join(this.trajectoriesDir, "index.json");
604
+ }
605
+ /**
606
+ * Initialize storage directories
607
+ */
608
+ async initialize() {
609
+ await mkdir(this.trajectoriesDir, { recursive: true });
610
+ await mkdir(this.activeDir, { recursive: true });
611
+ await mkdir(this.completedDir, { recursive: true });
612
+ if (!existsSync(this.indexPath)) {
613
+ await this.saveIndex({
614
+ version: 1,
615
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
616
+ trajectories: {}
617
+ });
618
+ }
619
+ await this.reconcileIndex();
620
+ }
621
+ /**
622
+ * Scan active/ and completed/ recursively and add any trajectory files
623
+ * missing from the index. Existing entries are preserved — reconcile
624
+ * only adds, never removes.
625
+ *
626
+ * Handles three on-disk layouts in completed/:
627
+ * - flat: completed/{id}.json (legacy workforce data)
628
+ * - monthly: completed/YYYY-MM/{id}.json (current save() writes)
629
+ * - nested: completed/.../{id}.json (defensive — any depth)
630
+ *
631
+ * Returns a ReconcileSummary so tests and CLI wrappers can observe
632
+ * outcomes without parsing logs. Only writes the index if anything was
633
+ * added.
634
+ */
635
+ async reconcileIndex() {
636
+ const summary = {
637
+ scanned: 0,
638
+ added: 0,
639
+ alreadyIndexed: 0,
640
+ skippedMalformedJson: 0,
641
+ skippedSchemaViolation: 0,
642
+ skippedIoError: 0
643
+ };
644
+ const index = await this.loadIndex();
645
+ const before = Object.keys(index.trajectories).length;
646
+ const discovered = [];
647
+ try {
648
+ const activeFiles = await readdir(this.activeDir);
649
+ for (const file of activeFiles) {
650
+ if (!file.endsWith(".json")) continue;
651
+ discovered.push(join(this.activeDir, file));
652
+ }
653
+ } catch (error) {
654
+ if (error.code !== "ENOENT") throw error;
655
+ }
656
+ await this.walkJsonFilesInto(this.completedDir, discovered);
657
+ for (const filePath of discovered) {
658
+ summary.scanned += 1;
659
+ const result = await this.readTrajectoryFile(filePath);
660
+ if (!result.ok) {
661
+ if (result.reason === "malformed_json") {
662
+ summary.skippedMalformedJson += 1;
663
+ } else if (result.reason === "schema_violation") {
664
+ summary.skippedSchemaViolation += 1;
665
+ } else {
666
+ summary.skippedIoError += 1;
667
+ }
668
+ continue;
669
+ }
670
+ const trajectory = result.trajectory;
671
+ if (index.trajectories[trajectory.id]) {
672
+ summary.alreadyIndexed += 1;
673
+ continue;
674
+ }
675
+ index.trajectories[trajectory.id] = {
676
+ title: trajectory.task.title,
677
+ status: trajectory.status,
678
+ startedAt: trajectory.startedAt,
679
+ completedAt: trajectory.completedAt,
680
+ path: filePath
681
+ };
682
+ summary.added += 1;
683
+ }
684
+ if (Object.keys(index.trajectories).length !== before) {
685
+ await this.saveIndex(index);
686
+ }
687
+ const hadSkips = summary.skippedMalformedJson + summary.skippedSchemaViolation + summary.skippedIoError > 0;
688
+ if (summary.added > 0 || hadSkips) {
689
+ const parts = [`reconciled ${summary.added}/${summary.scanned}`];
690
+ if (summary.skippedMalformedJson > 0) {
691
+ parts.push(`malformed: ${summary.skippedMalformedJson}`);
692
+ }
693
+ if (summary.skippedSchemaViolation > 0) {
694
+ parts.push(`invalid: ${summary.skippedSchemaViolation}`);
695
+ }
696
+ if (summary.skippedIoError > 0) {
697
+ parts.push(`io: ${summary.skippedIoError}`);
698
+ }
699
+ console.warn(`[trajectories] ${parts.join(", ")}`);
700
+ }
701
+ return summary;
702
+ }
703
+ /**
704
+ * Recursively collect all .json file paths under `dir` into `out`.
705
+ * Silently treats a missing directory as empty.
706
+ */
707
+ async walkJsonFilesInto(dir, out) {
708
+ let entries;
709
+ try {
710
+ entries = await readdir(dir, { withFileTypes: true });
711
+ } catch (error) {
712
+ if (error.code === "ENOENT") return;
713
+ throw error;
714
+ }
715
+ for (const entry of entries) {
716
+ const entryPath = join(dir, entry.name);
717
+ if (entry.isDirectory()) {
718
+ await this.walkJsonFilesInto(entryPath, out);
719
+ } else if (entry.isFile() && entry.name.endsWith(".json")) {
720
+ out.push(entryPath);
721
+ }
722
+ }
723
+ }
724
+ /**
725
+ * Save a trajectory.
726
+ *
727
+ * Validates the input against the trajectory schema before touching
728
+ * disk. Closes the historical read/write asymmetry where save() would
729
+ * happily write data that the reader then rejected, producing files
730
+ * that could never be loaded back.
731
+ */
732
+ async save(input) {
733
+ const validation = validateTrajectory(input);
734
+ if (!validation.success) {
735
+ const issues = validation.errors?.issues.map((issue) => {
736
+ const path2 = issue.path.length > 0 ? issue.path.join(".") : "root";
737
+ return `${path2}: ${issue.message}`;
738
+ }).join("; ") ?? "unknown validation error";
739
+ throw new Error(`Cannot save invalid trajectory: ${issues}`);
740
+ }
741
+ const trajectory = validation.data;
742
+ const isCompleted = trajectory.status === "completed" || trajectory.status === "abandoned";
743
+ let filePath;
744
+ if (isCompleted) {
745
+ const date = new Date(trajectory.completedAt ?? trajectory.startedAt);
746
+ const monthDir = join(
747
+ this.completedDir,
748
+ `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`
749
+ );
750
+ await mkdir(monthDir, { recursive: true });
751
+ filePath = join(monthDir, `${trajectory.id}.json`);
752
+ const activePath = join(this.activeDir, `${trajectory.id}.json`);
753
+ if (existsSync(activePath)) {
754
+ await unlink(activePath);
755
+ }
756
+ const mdPath = join(monthDir, `${trajectory.id}.md`);
757
+ const markdown = exportToMarkdown(trajectory);
758
+ await writeFile(mdPath, markdown, "utf-8");
759
+ } else {
760
+ filePath = join(this.activeDir, `${trajectory.id}.json`);
761
+ }
762
+ await writeFile(filePath, JSON.stringify(trajectory, null, 2), "utf-8");
763
+ await this.updateIndex(trajectory, filePath);
764
+ }
765
+ /**
766
+ * Get a trajectory by ID
767
+ */
768
+ async get(id) {
769
+ const activePath = join(this.activeDir, `${id}.json`);
770
+ if (existsSync(activePath)) {
771
+ return this.readTrajectoryOrNull(activePath);
772
+ }
773
+ const index = await this.loadIndex();
774
+ const entry = index.trajectories[id];
775
+ if (entry?.path && existsSync(entry.path)) {
776
+ return this.readTrajectoryOrNull(entry.path);
777
+ }
778
+ try {
779
+ const flatPath = join(this.completedDir, `${id}.json`);
780
+ if (existsSync(flatPath)) {
781
+ return this.readTrajectoryOrNull(flatPath);
782
+ }
783
+ const months = await readdir(this.completedDir);
784
+ for (const month of months) {
785
+ const filePath = join(this.completedDir, month, `${id}.json`);
786
+ if (existsSync(filePath)) {
787
+ return this.readTrajectoryOrNull(filePath);
788
+ }
789
+ }
790
+ } catch (error) {
791
+ if (error.code !== "ENOENT") {
792
+ console.error("Error searching completed trajectories:", error);
793
+ }
794
+ }
795
+ return null;
796
+ }
797
+ /**
798
+ * Get the currently active trajectory
799
+ */
800
+ async getActive() {
801
+ try {
802
+ const files = await readdir(this.activeDir);
803
+ const jsonFiles = files.filter((f) => f.endsWith(".json"));
804
+ if (jsonFiles.length === 0) {
805
+ return null;
806
+ }
807
+ let mostRecent = null;
808
+ let mostRecentTime = 0;
809
+ for (const file of jsonFiles) {
810
+ const trajectory = await this.readTrajectoryOrNull(
811
+ join(this.activeDir, file)
812
+ );
813
+ if (trajectory) {
814
+ const startTime = new Date(trajectory.startedAt).getTime();
815
+ if (startTime > mostRecentTime) {
816
+ mostRecentTime = startTime;
817
+ mostRecent = trajectory;
818
+ }
819
+ }
820
+ }
821
+ return mostRecent;
822
+ } catch (error) {
823
+ if (error.code === "ENOENT") {
824
+ return null;
825
+ }
826
+ console.error("Error reading active trajectories:", error);
827
+ return null;
828
+ }
829
+ }
830
+ /**
831
+ * List trajectories with optional filtering
832
+ */
833
+ async list(query) {
834
+ const index = await this.loadIndex();
835
+ let entries = Object.entries(index.trajectories);
836
+ if (query.status) {
837
+ entries = entries.filter(([, entry]) => entry.status === query.status);
838
+ }
839
+ if (query.since) {
840
+ const sinceTime = new Date(query.since).getTime();
841
+ entries = entries.filter(
842
+ ([, entry]) => new Date(entry.startedAt).getTime() >= sinceTime
843
+ );
844
+ }
845
+ if (query.until) {
846
+ const untilTime = new Date(query.until).getTime();
847
+ entries = entries.filter(
848
+ ([, entry]) => new Date(entry.startedAt).getTime() <= untilTime
849
+ );
850
+ }
851
+ const sortBy = query.sortBy ?? "startedAt";
852
+ const sortOrder = query.sortOrder ?? "desc";
853
+ entries.sort((a, b) => {
854
+ const aVal = a[1][sortBy] ?? "";
855
+ const bVal = b[1][sortBy] ?? "";
856
+ const cmp = String(aVal).localeCompare(String(bVal));
857
+ return sortOrder === "asc" ? cmp : -cmp;
858
+ });
859
+ const offset = query.offset ?? 0;
860
+ const limit = query.limit ?? 50;
861
+ entries = entries.slice(offset, offset + limit);
862
+ return Promise.all(
863
+ entries.map(async ([id, entry]) => {
864
+ const trajectory = await this.get(id);
865
+ return {
866
+ id,
867
+ title: entry.title,
868
+ status: entry.status,
869
+ startedAt: entry.startedAt,
870
+ completedAt: entry.completedAt,
871
+ confidence: trajectory?.retrospective?.confidence,
872
+ chapterCount: trajectory?.chapters.length ?? 0,
873
+ decisionCount: trajectory?.chapters.reduce(
874
+ (count, chapter) => count + chapter.events.filter((e) => e.type === "decision").length,
875
+ 0
876
+ ) ?? 0
877
+ };
878
+ })
879
+ );
880
+ }
881
+ /**
882
+ * Delete a trajectory
883
+ */
884
+ async delete(id) {
885
+ const activePath = join(this.activeDir, `${id}.json`);
886
+ if (existsSync(activePath)) {
887
+ await unlink(activePath);
888
+ }
889
+ const index = await this.loadIndex();
890
+ const entry = index.trajectories[id];
891
+ if (entry?.path && existsSync(entry.path)) {
892
+ await unlink(entry.path);
893
+ const mdPath = entry.path.replace(".json", ".md");
894
+ if (existsSync(mdPath)) {
895
+ await unlink(mdPath);
896
+ }
897
+ }
898
+ delete index.trajectories[id];
899
+ await this.saveIndex(index);
900
+ }
901
+ /**
902
+ * Search trajectories by text
903
+ */
904
+ async search(text, options) {
905
+ const allTrajectories = await this.list({});
906
+ const searchLower = text.toLowerCase();
907
+ const limit = options?.limit ?? 20;
908
+ const matches = [];
909
+ for (const summary of allTrajectories) {
910
+ if (matches.length >= limit) break;
911
+ if (summary.title.toLowerCase().includes(searchLower)) {
912
+ matches.push(summary);
913
+ continue;
914
+ }
915
+ const trajectory = await this.get(summary.id);
916
+ if (!trajectory) continue;
917
+ if (trajectory.retrospective?.summary.toLowerCase().includes(searchLower)) {
918
+ matches.push(summary);
919
+ continue;
920
+ }
921
+ const hasMatchingDecision = trajectory.chapters.some(
922
+ (chapter) => chapter.events.some(
923
+ (event) => event.type === "decision" && event.content.toLowerCase().includes(searchLower)
924
+ )
925
+ );
926
+ if (hasMatchingDecision) {
927
+ matches.push(summary);
928
+ }
929
+ }
930
+ return matches;
931
+ }
932
+ /**
933
+ * Close storage (no-op for file storage)
934
+ */
935
+ async close() {
936
+ }
937
+ // Private helpers
938
+ /**
939
+ * Read a trajectory file and return a tagged result so callers can
940
+ * distinguish missing files, malformed JSON, and schema violations.
941
+ *
942
+ * Does NOT log. Callers choose whether to warn, swallow, or throw.
943
+ */
944
+ async readTrajectoryFile(path2) {
945
+ let content;
946
+ try {
947
+ content = await readFile(path2, "utf-8");
948
+ } catch (error) {
949
+ return { ok: false, reason: "io_error", path: path2, error };
950
+ }
951
+ let data;
952
+ try {
953
+ data = JSON.parse(content);
954
+ } catch (error) {
955
+ return { ok: false, reason: "malformed_json", path: path2, error };
956
+ }
957
+ const validation = validateTrajectory(data);
958
+ if (validation.success) {
959
+ return { ok: true, trajectory: validation.data };
960
+ }
961
+ return {
962
+ ok: false,
963
+ reason: "schema_violation",
964
+ path: path2,
965
+ error: validation.errors
966
+ };
967
+ }
968
+ /**
969
+ * Convenience wrapper for callers that only care whether they got a
970
+ * trajectory. Returns null for any failure and writes nothing to the
971
+ * console — so nothing leaks into test output or the CLI spinner.
972
+ */
973
+ async readTrajectoryOrNull(path2) {
974
+ const result = await this.readTrajectoryFile(path2);
975
+ return result.ok ? result.trajectory : null;
976
+ }
977
+ async loadIndex() {
978
+ try {
979
+ const content = await readFile(this.indexPath, "utf-8");
980
+ return JSON.parse(content);
981
+ } catch (error) {
982
+ if (error.code !== "ENOENT") {
983
+ console.error(
984
+ "Error loading trajectory index, using empty index:",
985
+ error
986
+ );
987
+ }
988
+ return {
989
+ version: 1,
990
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
991
+ trajectories: {}
992
+ };
993
+ }
994
+ }
995
+ async saveIndex(index) {
996
+ index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
997
+ await writeFile(this.indexPath, JSON.stringify(index, null, 2), "utf-8");
998
+ }
999
+ async updateIndex(trajectory, filePath) {
1000
+ const index = await this.loadIndex();
1001
+ index.trajectories[trajectory.id] = {
1002
+ title: trajectory.task.title,
1003
+ status: trajectory.status,
1004
+ startedAt: trajectory.startedAt,
1005
+ completedAt: trajectory.completedAt,
1006
+ path: filePath
1007
+ };
1008
+ await this.saveIndex(index);
1009
+ }
1010
+ };
1011
+
1012
+ // src/cli/commands/abandon.ts
1013
+ function registerAbandonCommand(program2) {
1014
+ program2.command("abandon").description("Abandon the active trajectory").option("-r, --reason <text>", "Reason for abandonment").action(async (options) => {
1015
+ const storage = new FileStorage();
1016
+ await storage.initialize();
1017
+ const active = await storage.getActive();
1018
+ if (!active) {
1019
+ console.error("Error: No active trajectory");
1020
+ throw new Error("No active trajectory");
1021
+ }
1022
+ const abandoned = abandonTrajectory(active, options.reason);
1023
+ await storage.save(abandoned);
1024
+ console.log(`\u2713 Trajectory abandoned: ${abandoned.id}`);
1025
+ if (options.reason) {
1026
+ console.log(` Reason: ${options.reason}`);
1027
+ }
1028
+ });
1029
+ }
1030
+
1031
+ // src/cli/commands/compact.ts
1032
+ import { execFileSync } from "child_process";
1033
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
1034
+ import { dirname, join as join4 } from "path";
1035
+
1036
+ // src/compact/config.ts
1037
+ import { existsSync as existsSync2, readFileSync } from "fs";
1038
+ import { join as join2 } from "path";
1039
+ var DEFAULT_CONFIG = {
1040
+ provider: "auto",
1041
+ model: void 0,
1042
+ maxInputTokens: 3e4,
1043
+ maxOutputTokens: 4e3,
1044
+ temperature: 0.3
1045
+ };
1046
+ function getCompactionConfig() {
1047
+ const fileConfig = loadFileConfig();
1048
+ return {
1049
+ provider: readStringEnv("TRAJECTORIES_LLM_PROVIDER") ?? readString(fileConfig.provider) ?? DEFAULT_CONFIG.provider,
1050
+ model: readStringEnv("TRAJECTORIES_LLM_MODEL") ?? readString(fileConfig.model) ?? DEFAULT_CONFIG.model,
1051
+ maxInputTokens: readNumberEnv("TRAJECTORIES_LLM_MAX_INPUT_TOKENS") ?? readNumber(fileConfig.maxInputTokens) ?? DEFAULT_CONFIG.maxInputTokens,
1052
+ maxOutputTokens: readNumberEnv("TRAJECTORIES_LLM_MAX_OUTPUT_TOKENS") ?? readNumber(fileConfig.maxOutputTokens) ?? DEFAULT_CONFIG.maxOutputTokens,
1053
+ temperature: readNumberEnv("TRAJECTORIES_LLM_TEMPERATURE") ?? readNumber(fileConfig.temperature) ?? DEFAULT_CONFIG.temperature
1054
+ };
1055
+ }
1056
+ function loadFileConfig() {
1057
+ const configPath = join2(getPrimaryConfigDir(), "config.json");
1058
+ if (!existsSync2(configPath)) {
1059
+ return {};
1060
+ }
1061
+ try {
1062
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
1063
+ if (!isRecord(raw)) {
1064
+ return {};
1065
+ }
1066
+ const merged = {};
1067
+ for (const section of [raw, raw.compaction, raw.llm]) {
1068
+ if (!isRecord(section)) {
1069
+ continue;
1070
+ }
1071
+ for (const [key, value] of Object.entries(section)) {
1072
+ if (key === "compaction" || key === "llm") {
1073
+ continue;
1074
+ }
1075
+ merged[key] = value;
1076
+ }
1077
+ }
1078
+ return {
1079
+ provider: readString(merged.provider),
1080
+ model: readString(merged.model),
1081
+ maxInputTokens: readNumber(merged.maxInputTokens),
1082
+ maxOutputTokens: readNumber(merged.maxOutputTokens),
1083
+ temperature: readNumber(merged.temperature)
1084
+ };
1085
+ } catch {
1086
+ return {};
1087
+ }
1088
+ }
1089
+ function getPrimaryConfigDir() {
1090
+ const searchPaths = getSearchPaths();
1091
+ return searchPaths[0] ?? join2(process.cwd(), ".trajectories");
1092
+ }
1093
+ function readStringEnv(name) {
1094
+ return readString(process.env[name]);
1095
+ }
1096
+ function readNumberEnv(name) {
1097
+ return readNumber(process.env[name]);
1098
+ }
1099
+ function readString(value) {
1100
+ if (typeof value !== "string") {
1101
+ return void 0;
1102
+ }
1103
+ const trimmed = value.trim();
1104
+ return trimmed.length > 0 ? trimmed : void 0;
1105
+ }
1106
+ function readNumber(value) {
1107
+ if (typeof value === "number" && Number.isFinite(value)) {
1108
+ return value;
1109
+ }
1110
+ if (typeof value !== "string") {
1111
+ return void 0;
1112
+ }
1113
+ const trimmed = value.trim();
1114
+ if (trimmed.length === 0) {
1115
+ return void 0;
1116
+ }
1117
+ const parsed = Number(trimmed);
1118
+ return Number.isFinite(parsed) ? parsed : void 0;
1119
+ }
1120
+ function isRecord(value) {
1121
+ return typeof value === "object" && value !== null;
1122
+ }
1123
+
1124
+ // src/compact/markdown.ts
1125
+ function generateCompactionMarkdown(compacted) {
1126
+ const dateRange = `${formatDate2(compacted.dateRange.start)} - ${formatDate2(compacted.dateRange.end)}`;
1127
+ const agents = compacted.summary.uniqueAgents.length > 0 ? compacted.summary.uniqueAgents.join(", ") : "None";
1128
+ const decisionRows = compacted.decisions.length > 0 ? compacted.decisions.map(
1129
+ (decision) => `| ${escapeTableCell(decision.question)} | ${escapeTableCell(decision.chosen)} | ${escapeTableCell(decision.impact)} |`
1130
+ ).join("\n") : "| None identified | | |";
1131
+ const conventions = compacted.conventions.length > 0 ? compacted.conventions.map(
1132
+ (convention) => `- **${convention.pattern || "Unnamed pattern"}**: ${convention.rationale || "No rationale captured."} (scope: ${convention.scope || "unspecified"})`
1133
+ ).join("\n") : "- None established.";
1134
+ const lessons = compacted.lessons.length > 0 ? compacted.lessons.map((lesson) => {
1135
+ const context = lesson.context ? ` (${lesson.context})` : "";
1136
+ const recommendation = lesson.recommendation ? ` - ${lesson.recommendation}` : "";
1137
+ return `- ${lesson.lesson}${context}${recommendation}`;
1138
+ }).join("\n") : "- None captured.";
1139
+ const openQuestions = compacted.openQuestions.length > 0 ? compacted.openQuestions.map((question) => `- ${question}`).join("\n") : "- None.";
1140
+ return [
1141
+ `# Trajectory Compaction: ${dateRange}`,
1142
+ "",
1143
+ "## Summary",
1144
+ compacted.narrative || "No narrative available.",
1145
+ "",
1146
+ `## Key Decisions (${compacted.decisions.length})`,
1147
+ "| Question | Decision | Impact |",
1148
+ "|----------|----------|--------|",
1149
+ decisionRows,
1150
+ "",
1151
+ "## Conventions Established",
1152
+ conventions,
1153
+ "",
1154
+ "## Lessons Learned",
1155
+ lessons,
1156
+ "",
1157
+ "## Open Questions",
1158
+ openQuestions,
1159
+ "",
1160
+ "## Stats",
1161
+ `- Sessions: ${compacted.sourceTrajectories.length}, Agents: ${agents}, Files: ${compacted.filesAffected.length}, Commits: ${compacted.commits.length}`,
1162
+ `- Date range: ${compacted.dateRange.start} - ${compacted.dateRange.end}`
1163
+ ].join("\n");
1164
+ }
1165
+ function formatDate2(value) {
1166
+ const date = new Date(value);
1167
+ if (Number.isNaN(date.getTime())) {
1168
+ return value;
1169
+ }
1170
+ return date.toISOString().slice(0, 10);
1171
+ }
1172
+ function escapeTableCell(value) {
1173
+ return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
1174
+ }
1175
+
1176
+ // src/compact/parser.ts
1177
+ function parseCompactionResponse(llmOutput) {
1178
+ const trimmed = llmOutput.trim();
1179
+ const parsedJson = parseJsonCandidate(trimmed) ?? parseJsonCandidate(extractFirstMarkdownJsonBlock(trimmed)) ?? parseJsonCandidate(extractBalancedJsonObject(trimmed));
1180
+ if (parsedJson) {
1181
+ return normalizeCompactionOutput(parsedJson, trimmed);
1182
+ }
1183
+ return normalizeCompactionOutput(extractFromProse(trimmed), trimmed);
1184
+ }
1185
+ function mergeCompactionWithMetadata(metadata, llmOutput) {
1186
+ return {
1187
+ ...metadata,
1188
+ ...llmOutput
1189
+ };
1190
+ }
1191
+ function parseJsonCandidate(candidate) {
1192
+ if (!candidate) {
1193
+ return null;
1194
+ }
1195
+ try {
1196
+ return JSON.parse(candidate);
1197
+ } catch {
1198
+ return null;
1199
+ }
1200
+ }
1201
+ function extractFirstMarkdownJsonBlock(text) {
1202
+ const match = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
1203
+ return match ? match[1].trim() : null;
1204
+ }
1205
+ function extractBalancedJsonObject(text) {
1206
+ const start = text.indexOf("{");
1207
+ if (start === -1) {
1208
+ return null;
1209
+ }
1210
+ let depth = 0;
1211
+ let inString = false;
1212
+ let escaped = false;
1213
+ for (let index = start; index < text.length; index += 1) {
1214
+ const char = text[index];
1215
+ if (escaped) {
1216
+ escaped = false;
1217
+ continue;
1218
+ }
1219
+ if (char === "\\") {
1220
+ escaped = true;
1221
+ continue;
1222
+ }
1223
+ if (char === '"') {
1224
+ inString = !inString;
1225
+ continue;
1226
+ }
1227
+ if (inString) {
1228
+ continue;
1229
+ }
1230
+ if (char === "{") {
1231
+ depth += 1;
1232
+ } else if (char === "}") {
1233
+ depth -= 1;
1234
+ if (depth === 0) {
1235
+ return text.slice(start, index + 1);
1236
+ }
1237
+ }
1238
+ }
1239
+ return null;
1240
+ }
1241
+ function extractFromProse(text) {
1242
+ const sections = splitSections(text);
1243
+ const narrativeSection = sections.narrative ?? sections.summary ?? leadingNarrative(text);
1244
+ return {
1245
+ narrative: normalizeText(narrativeSection),
1246
+ decisions: parseDecisionSection(
1247
+ sections["key decisions"] ?? sections.decisions ?? ""
1248
+ ),
1249
+ conventions: parseConventionSection(
1250
+ sections["conventions established"] ?? sections.conventions ?? ""
1251
+ ),
1252
+ lessons: parseLessonSection(
1253
+ sections["lessons learned"] ?? sections.lessons ?? ""
1254
+ ),
1255
+ openQuestions: parseStringList(
1256
+ sections["open questions"] ?? sections.questions ?? ""
1257
+ )
1258
+ };
1259
+ }
1260
+ function splitSections(text) {
1261
+ const matches = [...text.matchAll(/^##+\s+(.+?)\s*$/gm)];
1262
+ const sections = {};
1263
+ for (let index = 0; index < matches.length; index += 1) {
1264
+ const current = matches[index];
1265
+ const next = matches[index + 1];
1266
+ const title = normalizeHeading(current[1]);
1267
+ const start = current.index === void 0 ? 0 : current.index + current[0].length;
1268
+ const end = next?.index ?? text.length;
1269
+ sections[title] = text.slice(start, end).trim();
1270
+ }
1271
+ return sections;
1272
+ }
1273
+ function normalizeHeading(value) {
1274
+ return value.toLowerCase().replace(/\(\d+\)/g, "").replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
1275
+ }
1276
+ function leadingNarrative(text) {
1277
+ const beforeHeading = text.split(/^##+\s+/m, 1)[0] ?? "";
1278
+ const withoutCode = beforeHeading.replace(/```[\s\S]*?```/g, "").trim();
1279
+ return withoutCode;
1280
+ }
1281
+ function normalizeCompactionOutput(raw, fallbackNarrativeSource) {
1282
+ const candidate = isRecord2(raw) ? raw : {};
1283
+ const narrative = normalizeText(
1284
+ typeof candidate.narrative === "string" ? candidate.narrative : typeof candidate.summary === "string" ? candidate.summary : typeof candidate.overview === "string" ? candidate.overview : leadingNarrative(fallbackNarrativeSource)
1285
+ );
1286
+ return {
1287
+ narrative: narrative || normalizeText(fallbackNarrativeSource) || "No narrative provided.",
1288
+ decisions: normalizeDecisionArray(candidate.decisions),
1289
+ conventions: normalizeConventionArray(candidate.conventions),
1290
+ lessons: normalizeLessonArray(candidate.lessons),
1291
+ openQuestions: normalizeStringArray(
1292
+ candidate.openQuestions ?? candidate.open_questions ?? candidate.questions
1293
+ )
1294
+ };
1295
+ }
1296
+ function normalizeDecisionArray(value) {
1297
+ if (!Array.isArray(value)) {
1298
+ return [];
1299
+ }
1300
+ return value.map((entry) => {
1301
+ if (typeof entry === "string") {
1302
+ return {
1303
+ question: normalizeText(entry),
1304
+ chosen: "",
1305
+ reasoning: "",
1306
+ impact: ""
1307
+ };
1308
+ }
1309
+ if (!isRecord2(entry)) {
1310
+ return null;
1311
+ }
1312
+ return {
1313
+ question: readString2(entry, ["question", "prompt", "topic"]),
1314
+ chosen: readString2(entry, ["chosen", "decision", "answer"]),
1315
+ reasoning: readString2(entry, ["reasoning", "why", "rationale"]),
1316
+ impact: readString2(entry, ["impact", "result", "outcome"])
1317
+ };
1318
+ }).filter((entry) => {
1319
+ return entry !== null && hasContent(Object.values(entry));
1320
+ });
1321
+ }
1322
+ function normalizeConventionArray(value) {
1323
+ if (!Array.isArray(value)) {
1324
+ return [];
1325
+ }
1326
+ return value.map((entry) => {
1327
+ if (typeof entry === "string") {
1328
+ return {
1329
+ pattern: normalizeText(entry),
1330
+ rationale: "",
1331
+ scope: ""
1332
+ };
1333
+ }
1334
+ if (!isRecord2(entry)) {
1335
+ return null;
1336
+ }
1337
+ return {
1338
+ pattern: readString2(entry, ["pattern", "rule", "convention"]),
1339
+ rationale: readString2(entry, ["rationale", "reasoning", "why"]),
1340
+ scope: readString2(entry, ["scope", "appliesTo", "applies_to"])
1341
+ };
1342
+ }).filter((entry) => {
1343
+ return entry !== null && hasContent(Object.values(entry));
1344
+ });
1345
+ }
1346
+ function normalizeLessonArray(value) {
1347
+ if (!Array.isArray(value)) {
1348
+ return [];
1349
+ }
1350
+ return value.map((entry) => {
1351
+ if (typeof entry === "string") {
1352
+ return {
1353
+ lesson: normalizeText(entry),
1354
+ context: "",
1355
+ recommendation: ""
1356
+ };
1357
+ }
1358
+ if (!isRecord2(entry)) {
1359
+ return null;
1360
+ }
1361
+ return {
1362
+ lesson: readString2(entry, ["lesson", "learning", "takeaway"]),
1363
+ context: readString2(entry, ["context", "situation", "when"]),
1364
+ recommendation: readString2(entry, [
1365
+ "recommendation",
1366
+ "suggestion",
1367
+ "nextStep",
1368
+ "next_step"
1369
+ ])
1370
+ };
1371
+ }).filter((entry) => {
1372
+ return entry !== null && hasContent(Object.values(entry));
1373
+ });
1374
+ }
1375
+ function normalizeStringArray(value) {
1376
+ if (!Array.isArray(value)) {
1377
+ return [];
1378
+ }
1379
+ return value.map((entry) => typeof entry === "string" ? normalizeText(entry) : "").filter(Boolean);
1380
+ }
1381
+ function parseDecisionSection(section) {
1382
+ const tableDecisions = parseMarkdownTable(section).map((row) => ({
1383
+ question: row[0] ?? "",
1384
+ chosen: row[1] ?? "",
1385
+ reasoning: row[2] ?? "",
1386
+ impact: row[3] ?? row[2] ?? ""
1387
+ }));
1388
+ if (tableDecisions.length > 0) {
1389
+ return tableDecisions.filter((entry) => hasContent(Object.values(entry)));
1390
+ }
1391
+ return parseListItems(section).map((item) => {
1392
+ const fields = parseFieldMap(item);
1393
+ return {
1394
+ question: fields.question ?? fields.prompt ?? fields.topic ?? fields.title ?? item,
1395
+ chosen: fields.chosen ?? fields.decision ?? fields.answer ?? "",
1396
+ reasoning: fields.reasoning ?? fields.rationale ?? fields.why ?? "",
1397
+ impact: fields.impact ?? fields.outcome ?? fields.result ?? ""
1398
+ };
1399
+ }).filter((entry) => hasContent(Object.values(entry)));
1400
+ }
1401
+ function parseConventionSection(section) {
1402
+ return parseListItems(section).map((item) => {
1403
+ const emphasized = item.match(/^\*\*(.+?)\*\*:\s*(.+)$/);
1404
+ const scopeMatch = item.match(/\((?:scope|applies to):\s*([^)]+)\)\s*$/i);
1405
+ const withoutScope = scopeMatch ? item.slice(0, scopeMatch.index).trim() : item;
1406
+ if (emphasized) {
1407
+ return {
1408
+ pattern: normalizeText(emphasized[1]),
1409
+ rationale: normalizeText(
1410
+ withoutScope.replace(/^\*\*(.+?)\*\*:\s*/, "")
1411
+ ),
1412
+ scope: normalizeText(scopeMatch?.[1] ?? "")
1413
+ };
1414
+ }
1415
+ const fields = parseFieldMap(item);
1416
+ return {
1417
+ pattern: fields.pattern ?? fields.convention ?? fields.rule ?? item,
1418
+ rationale: fields.rationale ?? fields.reasoning ?? fields.why ?? "",
1419
+ scope: fields.scope ?? fields.applies ?? ""
1420
+ };
1421
+ }).filter((entry) => hasContent(Object.values(entry)));
1422
+ }
1423
+ function parseLessonSection(section) {
1424
+ return parseListItems(section).map((item) => {
1425
+ const fields = parseFieldMap(item);
1426
+ const dashParts = item.split(/\s[—-]\s/, 2);
1427
+ return {
1428
+ lesson: fields.lesson ?? fields.learning ?? fields.takeaway ?? dashParts[0] ?? item,
1429
+ context: fields.context ?? "",
1430
+ recommendation: fields.recommendation ?? fields.suggestion ?? fields.nextstep ?? dashParts[1] ?? ""
1431
+ };
1432
+ }).filter((entry) => hasContent(Object.values(entry)));
1433
+ }
1434
+ function parseStringList(section) {
1435
+ return parseListItems(section).map(normalizeText).filter(Boolean);
1436
+ }
1437
+ function parseMarkdownTable(section) {
1438
+ const lines = section.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("|"));
1439
+ if (lines.length < 2) {
1440
+ return [];
1441
+ }
1442
+ return lines.slice(1).filter((line) => !/^\|?\s*:?-{3,}/.test(line.replace(/\|/g, ""))).map(
1443
+ (line) => line.split("|").slice(1, -1).map((cell) => normalizeText(cell))
1444
+ );
1445
+ }
1446
+ function parseListItems(section) {
1447
+ return section.split("\n").map((line) => line.trim()).filter((line) => /^[-*] |\d+\.\s/.test(line)).map((line) => line.replace(/^[-*]\s+|\d+\.\s+/, "").trim()).filter(Boolean);
1448
+ }
1449
+ function parseFieldMap(item) {
1450
+ const normalized = item.replace(/\s+\|\s+/g, "; ");
1451
+ const segments = normalized.split(/;\s+/);
1452
+ const fields = {};
1453
+ for (const segment of segments) {
1454
+ const match = segment.match(/^([A-Za-z ]+):\s*(.+)$/);
1455
+ if (!match) {
1456
+ continue;
1457
+ }
1458
+ const key = match[1].toLowerCase().replace(/\s+/g, "");
1459
+ fields[key] = normalizeText(match[2]);
1460
+ }
1461
+ return fields;
1462
+ }
1463
+ function readString2(record, keys) {
1464
+ for (const key of keys) {
1465
+ const value = record[key];
1466
+ if (typeof value === "string") {
1467
+ return normalizeText(value);
1468
+ }
1469
+ }
1470
+ return "";
1471
+ }
1472
+ function isRecord2(value) {
1473
+ return typeof value === "object" && value !== null;
1474
+ }
1475
+ function normalizeText(value) {
1476
+ return value.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
1477
+ }
1478
+ function hasContent(values) {
1479
+ return values.some((value) => value.trim().length > 0);
1480
+ }
1481
+
1482
+ // src/compact/prompts.ts
1483
+ var COMPACTION_SYSTEM_PROMPT = `You are a technical analyst reviewing agent work sessions (trajectories).
1484
+ Your job is to produce a concise, insightful summary that captures:
1485
+ - What was accomplished and how
1486
+ - Key decisions and their reasoning
1487
+ - Patterns/conventions established that should be followed in future work
1488
+ - Lessons learned from challenges and failures
1489
+ - Open questions or unresolved issues
1490
+
1491
+ Be specific. Reference actual file paths, function names, and technical details.
1492
+ Don't be generic - this summary replaces the raw data.`;
1493
+ var COMPACTED_OUTPUT_SCHEMA = `{
1494
+ "narrative": "string",
1495
+ "decisions": [
1496
+ {
1497
+ "question": "string",
1498
+ "chosen": "string",
1499
+ "reasoning": "string",
1500
+ "impact": "string"
1501
+ }
1502
+ ],
1503
+ "conventions": [
1504
+ {
1505
+ "pattern": "string",
1506
+ "rationale": "string",
1507
+ "scope": "string"
1508
+ }
1509
+ ],
1510
+ "lessons": [
1511
+ {
1512
+ "lesson": "string",
1513
+ "context": "string",
1514
+ "recommendation": "string"
1515
+ }
1516
+ ],
1517
+ "openQuestions": ["string"]
1518
+ }`;
1519
+ function buildCompactionPrompt(serializedTrajectories, options = {}) {
1520
+ const focusAreas = options.focusAreas && options.focusAreas.length > 0 ? options.focusAreas.map((area) => `- ${area}`).join("\n") : [
1521
+ "- What work was attempted, completed, or abandoned",
1522
+ "- Why specific technical decisions were made",
1523
+ "- Which conventions should carry forward",
1524
+ "- What broke, what worked, and what should change next time"
1525
+ ].join("\n");
1526
+ const maxOutputInstruction = options.maxOutputTokens ? `Keep the full response within approximately ${options.maxOutputTokens} tokens while preserving technical specificity.` : "Keep the response concise, dense with signal, and avoid filler.";
1527
+ const userPrompt = [
1528
+ "Review the following serialized agent trajectories and return a single JSON object.",
1529
+ "The JSON must match this schema exactly:",
1530
+ COMPACTED_OUTPUT_SCHEMA,
1531
+ "",
1532
+ "Requirements:",
1533
+ "- Output raw JSON only. Do not wrap it in markdown fences.",
1534
+ "- `narrative` should be 2-3 tight paragraphs.",
1535
+ "- `decisions`, `conventions`, and `lessons` must always be arrays, even if empty.",
1536
+ "- Prefer concrete file paths, symbols, commands, and implementation details over generic summaries.",
1537
+ maxOutputInstruction,
1538
+ "",
1539
+ "Focus areas:",
1540
+ focusAreas,
1541
+ "",
1542
+ "Serialized trajectories:",
1543
+ serializedTrajectories.trim()
1544
+ ].join("\n");
1545
+ return [
1546
+ {
1547
+ role: "system",
1548
+ content: COMPACTION_SYSTEM_PROMPT
1549
+ },
1550
+ {
1551
+ role: "user",
1552
+ content: userPrompt
1553
+ }
1554
+ ];
1555
+ }
1556
+
1557
+ // src/compact/provider.ts
1558
+ import { execFile, spawn } from "child_process";
1559
+ import { constants, accessSync } from "fs";
1560
+ import { homedir } from "os";
1561
+ import { join as join3 } from "path";
1562
+ import { promisify } from "util";
1563
+ var execFileAsync = promisify(execFile);
1564
+ var DEFAULT_OPENAI_MODEL = "gpt-4o";
1565
+ var DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514";
1566
+ var DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
1567
+ var DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com";
1568
+ var DEFAULT_MAX_TOKENS = 4096;
1569
+ var OpenAIProvider = class {
1570
+ apiKey;
1571
+ model;
1572
+ baseUrl;
1573
+ constructor(config = {}) {
1574
+ this.apiKey = config.apiKey?.trim() || process.env.OPENAI_API_KEY?.trim() || "";
1575
+ this.model = normalizeModel(config.model) ?? normalizeModel(process.env.TRAJECTORIES_LLM_MODEL) ?? DEFAULT_OPENAI_MODEL;
1576
+ this.baseUrl = config.baseUrl ?? process.env.OPENAI_BASE_URL ?? DEFAULT_OPENAI_BASE_URL;
1577
+ if (this.baseUrl !== DEFAULT_OPENAI_BASE_URL) {
1578
+ console.warn(
1579
+ `[trajectories] OpenAI base URL overridden to: ${this.baseUrl}`
1580
+ );
1581
+ }
1582
+ if (!this.apiKey) {
1583
+ throw new Error("OPENAI_API_KEY is required for OpenAIProvider");
1584
+ }
1585
+ }
1586
+ async complete(messages, options = {}) {
1587
+ const controller = new AbortController();
1588
+ const timeout = setTimeout(() => controller.abort(), 3e5);
1589
+ try {
1590
+ const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
1591
+ method: "POST",
1592
+ headers: {
1593
+ Authorization: `Bearer ${this.apiKey}`,
1594
+ "Content-Type": "application/json"
1595
+ },
1596
+ body: JSON.stringify({
1597
+ model: this.model,
1598
+ messages,
1599
+ max_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
1600
+ temperature: options.temperature ?? 0.2,
1601
+ response_format: options.jsonMode ? { type: "json_object" } : void 0
1602
+ }),
1603
+ signal: controller.signal
1604
+ });
1605
+ const body = await parseJson(response);
1606
+ if (!response.ok) {
1607
+ throw new Error(
1608
+ body.error?.message ?? `OpenAI request failed with status ${response.status}`
1609
+ );
1610
+ }
1611
+ const content = body.choices?.[0]?.message?.content;
1612
+ if (!content) {
1613
+ throw new Error("OpenAI response did not include completion content");
1614
+ }
1615
+ return content;
1616
+ } finally {
1617
+ clearTimeout(timeout);
1618
+ }
1619
+ }
1620
+ };
1621
+ var AnthropicProvider = class {
1622
+ apiKey;
1623
+ model;
1624
+ baseUrl;
1625
+ constructor(config = {}) {
1626
+ this.apiKey = config.apiKey?.trim() || process.env.ANTHROPIC_API_KEY?.trim() || "";
1627
+ this.model = normalizeModel(config.model) ?? normalizeModel(process.env.TRAJECTORIES_LLM_MODEL) ?? DEFAULT_ANTHROPIC_MODEL;
1628
+ this.baseUrl = config.baseUrl ?? process.env.ANTHROPIC_BASE_URL ?? DEFAULT_ANTHROPIC_BASE_URL;
1629
+ if (this.baseUrl !== DEFAULT_ANTHROPIC_BASE_URL) {
1630
+ console.warn(
1631
+ `[trajectories] Anthropic base URL overridden to: ${this.baseUrl}`
1632
+ );
1633
+ }
1634
+ if (!this.apiKey) {
1635
+ throw new Error("ANTHROPIC_API_KEY is required for AnthropicProvider");
1636
+ }
1637
+ }
1638
+ async complete(messages, options = {}) {
1639
+ const systemMessages = messages.filter((message) => message.role === "system").map((message) => message.content.trim()).filter(Boolean);
1640
+ const conversation = messages.filter((message) => message.role !== "system").map((message) => ({
1641
+ role: message.role,
1642
+ content: message.content
1643
+ }));
1644
+ if (conversation.length === 0) {
1645
+ throw new Error("AnthropicProvider requires at least one user message");
1646
+ }
1647
+ const controller = new AbortController();
1648
+ const timeout = setTimeout(() => controller.abort(), 3e5);
1649
+ try {
1650
+ const response = await fetch(`${this.baseUrl}/v1/messages`, {
1651
+ method: "POST",
1652
+ headers: {
1653
+ "Content-Type": "application/json",
1654
+ "anthropic-version": "2024-10-22",
1655
+ "x-api-key": this.apiKey
1656
+ },
1657
+ body: JSON.stringify({
1658
+ model: this.model,
1659
+ system: systemMessages.length > 0 ? systemMessages.join("\n\n") : void 0,
1660
+ messages: conversation,
1661
+ max_tokens: options.maxTokens ?? DEFAULT_MAX_TOKENS,
1662
+ temperature: options.temperature ?? 0.2
1663
+ }),
1664
+ signal: controller.signal
1665
+ });
1666
+ const body = await parseJson(response);
1667
+ if (!response.ok) {
1668
+ throw new Error(
1669
+ body.error?.message ?? `Anthropic request failed with status ${response.status}`
1670
+ );
1671
+ }
1672
+ const textBlocks = (body.content ?? []).filter(
1673
+ (block) => block.type === "text" && typeof block.text === "string"
1674
+ );
1675
+ const content = textBlocks.map((block) => block.text).join("\n").trim();
1676
+ if (!content) {
1677
+ throw new Error("Anthropic response did not include text content");
1678
+ }
1679
+ return content;
1680
+ } finally {
1681
+ clearTimeout(timeout);
1682
+ }
1683
+ }
1684
+ };
1685
+ var SUPPORTED_CLIS = ["claude", "codex", "gemini", "opencode"];
1686
+ var CLIProvider = class {
1687
+ cli;
1688
+ binaryPath;
1689
+ constructor(cli, binaryPath) {
1690
+ this.cli = cli;
1691
+ this.binaryPath = binaryPath;
1692
+ }
1693
+ get cliName() {
1694
+ return this.cli;
1695
+ }
1696
+ async complete(messages, _options = {}) {
1697
+ const prompt = messagesToPrompt(messages);
1698
+ const args = buildCliArgs(this.cli);
1699
+ const output = await spawnWithStdin(this.binaryPath, args, prompt);
1700
+ if (!output) {
1701
+ throw new Error(`${this.cli} CLI returned empty output`);
1702
+ }
1703
+ return output;
1704
+ }
1705
+ };
1706
+ function messagesToPrompt(messages) {
1707
+ const systemParts = [];
1708
+ const conversationParts = [];
1709
+ for (const msg of messages) {
1710
+ if (msg.role === "system") {
1711
+ systemParts.push(msg.content.trim());
1712
+ } else {
1713
+ conversationParts.push(msg.content.trim());
1714
+ }
1715
+ }
1716
+ const parts = [];
1717
+ if (systemParts.length > 0) {
1718
+ parts.push(systemParts.join("\n\n"));
1719
+ }
1720
+ if (conversationParts.length > 0) {
1721
+ parts.push(conversationParts.join("\n\n"));
1722
+ }
1723
+ return parts.join("\n\n---\n\n");
1724
+ }
1725
+ function buildCliArgs(cli) {
1726
+ switch (cli) {
1727
+ case "claude":
1728
+ return ["-p", "--output-format", "text"];
1729
+ case "codex":
1730
+ return ["exec", "-q"];
1731
+ case "gemini":
1732
+ return ["-p"];
1733
+ case "opencode":
1734
+ return ["run", "--no-color"];
1735
+ }
1736
+ }
1737
+ function spawnWithStdin(command, args, input) {
1738
+ return new Promise((resolve, reject) => {
1739
+ const child = spawn(command, args, {
1740
+ timeout: 3e5,
1741
+ stdio: ["pipe", "pipe", "pipe"]
1742
+ });
1743
+ const chunks = [];
1744
+ child.stdout.on("data", (chunk) => chunks.push(chunk));
1745
+ let stderr = "";
1746
+ child.stderr.on("data", (chunk) => {
1747
+ stderr += chunk.toString();
1748
+ });
1749
+ child.on("error", reject);
1750
+ child.on("close", (code) => {
1751
+ if (code !== 0) {
1752
+ reject(
1753
+ new Error(`CLI exited with code ${code}: ${stderr.slice(0, 200)}`)
1754
+ );
1755
+ } else {
1756
+ resolve(Buffer.concat(chunks).toString().trim());
1757
+ }
1758
+ });
1759
+ child.stdin.write(input);
1760
+ child.stdin.end();
1761
+ });
1762
+ }
1763
+ async function resolveProvider(config = {}) {
1764
+ const explicitProvider = (config.provider ?? process.env.TRAJECTORIES_LLM_PROVIDER)?.toLowerCase();
1765
+ const model = normalizeModel(config.model);
1766
+ if (explicitProvider === "openai") {
1767
+ return process.env.OPENAI_API_KEY ? new OpenAIProvider({ model }) : null;
1768
+ }
1769
+ if (explicitProvider === "anthropic") {
1770
+ return process.env.ANTHROPIC_API_KEY ? new AnthropicProvider({ model }) : null;
1771
+ }
1772
+ if (explicitProvider === "cli") {
1773
+ return resolveCLIProvider();
1774
+ }
1775
+ if (explicitProvider && explicitProvider !== "auto") {
1776
+ return null;
1777
+ }
1778
+ const cliProvider = await resolveCLIProvider();
1779
+ if (cliProvider) {
1780
+ return cliProvider;
1781
+ }
1782
+ if (process.env.OPENAI_API_KEY) {
1783
+ return new OpenAIProvider({ model });
1784
+ }
1785
+ if (process.env.ANTHROPIC_API_KEY) {
1786
+ return new AnthropicProvider({ model });
1787
+ }
1788
+ return null;
1789
+ }
1790
+ var CLI_SEARCH_PATHS = [
1791
+ "~/.local/bin",
1792
+ "~/.claude/local",
1793
+ "/usr/local/bin",
1794
+ "/opt/homebrew/bin"
1795
+ ];
1796
+ async function resolveCLIProvider() {
1797
+ const requestedCli = process.env.TRAJECTORIES_LLM_CLI?.trim().toLowerCase();
1798
+ const clisToTry = (() => {
1799
+ if (!requestedCli) {
1800
+ return SUPPORTED_CLIS;
1801
+ }
1802
+ if (SUPPORTED_CLIS.includes(requestedCli)) {
1803
+ return [requestedCli];
1804
+ }
1805
+ console.warn(
1806
+ `[trajectories] Unsupported TRAJECTORIES_LLM_CLI value "${requestedCli}", falling back to auto-detect`
1807
+ );
1808
+ return SUPPORTED_CLIS;
1809
+ })();
1810
+ for (const cli of clisToTry) {
1811
+ const path2 = await findBinary(cli);
1812
+ if (path2) {
1813
+ return new CLIProvider(cli, path2);
1814
+ }
1815
+ }
1816
+ return null;
1817
+ }
1818
+ async function findBinary(name) {
1819
+ try {
1820
+ const { stdout } = await execFileAsync("which", [name]);
1821
+ const path2 = stdout.trim();
1822
+ if (path2) return path2;
1823
+ } catch {
1824
+ }
1825
+ const home = homedir();
1826
+ for (const dir of CLI_SEARCH_PATHS) {
1827
+ const expanded = dir.startsWith("~/") ? join3(home, dir.slice(2)) : dir;
1828
+ const candidate = join3(expanded, name);
1829
+ try {
1830
+ accessSync(candidate, constants.X_OK);
1831
+ return candidate;
1832
+ } catch {
1833
+ }
1834
+ }
1835
+ return void 0;
1836
+ }
1837
+ function normalizeModel(value) {
1838
+ if (typeof value !== "string") {
1839
+ return void 0;
1840
+ }
1841
+ const trimmed = value.trim();
1842
+ return trimmed.length > 0 ? trimmed : void 0;
1843
+ }
1844
+ async function parseJson(response) {
1845
+ const text = await response.text();
1846
+ if (!text) {
1847
+ return {};
1848
+ }
1849
+ try {
1850
+ return JSON.parse(text);
1851
+ } catch {
1852
+ throw new Error(
1853
+ `Invalid JSON response (status ${response.status}, length ${text.length})`
1854
+ );
1855
+ }
1856
+ }
1857
+
1858
+ // src/compact/serializer.ts
1859
+ var DEFAULT_MAX_TOKENS2 = 3e4;
1860
+ var CHARS_PER_TOKEN = 4;
1861
+ var INCLUDED_SIGNIFICANCE = /* @__PURE__ */ new Set([
1862
+ "medium",
1863
+ "high",
1864
+ "critical"
1865
+ ]);
1866
+ function serializeForLLM(trajectories, maxTokens = DEFAULT_MAX_TOKENS2) {
1867
+ const maxChars = Math.max(0, maxTokens * CHARS_PER_TOKEN);
1868
+ const sessions = trajectories.map(renderSession);
1869
+ let document = joinSessions(sessions);
1870
+ if (document.length <= maxChars || sessions.length === 0) {
1871
+ return document;
1872
+ }
1873
+ const fixedChars = sessions.reduce(
1874
+ (total, session) => total + session.header.length + session.agents.length + session.decisions.length + session.findings.length + session.retrospective.length + session.filesAndCommits.length,
1875
+ 0
1876
+ );
1877
+ const chapterChars = sessions.reduce(
1878
+ (total, session) => total + session.chapters.reduce((sum, chapter) => sum + chapter.length, 0),
1879
+ 0
1880
+ );
1881
+ const remainingChapterChars = maxChars - fixedChars;
1882
+ if (remainingChapterChars <= 0 || chapterChars === 0) {
1883
+ return truncateText(document, maxChars);
1884
+ }
1885
+ const ratio = Math.min(1, remainingChapterChars / chapterChars);
1886
+ const truncatedSessions = sessions.map((session) => ({
1887
+ ...session,
1888
+ chapters: truncateChapters(
1889
+ session.chapters,
1890
+ session.chapters.reduce((sum, chapter) => sum + chapter.length, 0),
1891
+ ratio
1892
+ )
1893
+ }));
1894
+ document = joinSessions(truncatedSessions);
1895
+ return document.length <= maxChars ? document : truncateText(document, maxChars);
1896
+ }
1897
+ function renderSession(trajectory) {
1898
+ const sessionTitle = trajectory.task.title.trim() || trajectory.id;
1899
+ const duration = formatDuration(trajectory.startedAt, trajectory.completedAt);
1900
+ const header = [
1901
+ `## Session: ${sessionTitle} (${trajectory.status}, ${duration})`,
1902
+ trajectory.task.description ? `Description: ${trajectory.task.description}` : "",
1903
+ `Started: ${trajectory.startedAt}`,
1904
+ trajectory.completedAt ? `Completed: ${trajectory.completedAt}` : ""
1905
+ ].filter(Boolean).join("\n").concat("\n");
1906
+ const agents = trajectory.agents.length > 0 ? `Agents: ${trajectory.agents.map((agent) => `${agent.name} (${agent.role})`).join(", ")}
1907
+ ` : "Agents: none recorded\n";
1908
+ const chapters = trajectory.chapters.map(renderChapter);
1909
+ const decisions = renderDecisions(trajectory);
1910
+ const findings = renderFindings(trajectory);
1911
+ const retrospective = renderRetrospective(trajectory.retrospective);
1912
+ const filesAndCommits = [
1913
+ `Files changed: ${formatList(trajectory.filesChanged)}`,
1914
+ `Commits: ${formatList(trajectory.commits)}`
1915
+ ].join("\n").concat("\n");
1916
+ return {
1917
+ header,
1918
+ agents,
1919
+ chapters,
1920
+ decisions,
1921
+ findings,
1922
+ retrospective,
1923
+ filesAndCommits
1924
+ };
1925
+ }
1926
+ function renderChapter(chapter) {
1927
+ const lines = chapter.events.filter(shouldIncludeEvent).map((event) => formatEvent(event));
1928
+ const chapterBody = lines.length > 0 ? lines.map((line) => `- ${line}`).join("\n") : "- No medium/high/critical events captured";
1929
+ return [
1930
+ `### Chapter: ${chapter.title}`,
1931
+ `Agent: ${chapter.agentName}`,
1932
+ `Window: ${chapter.startedAt} -> ${chapter.endedAt ?? "ongoing"}`,
1933
+ chapterBody
1934
+ ].join("\n").concat("\n");
1935
+ }
1936
+ function renderDecisions(trajectory) {
1937
+ const seen = /* @__PURE__ */ new Set();
1938
+ const decisions = [];
1939
+ for (const chapter of trajectory.chapters) {
1940
+ for (const event of chapter.events) {
1941
+ if (event.type !== "decision") {
1942
+ continue;
1943
+ }
1944
+ const decision = asDecision(event.raw);
1945
+ if (!decision) {
1946
+ continue;
1947
+ }
1948
+ const key = `${decision.question}
1949
+ ${decision.chosen}
1950
+ ${decision.reasoning}`;
1951
+ if (!seen.has(key)) {
1952
+ seen.add(key);
1953
+ decisions.push(decision);
1954
+ }
1955
+ }
1956
+ }
1957
+ for (const decision of trajectory.retrospective?.decisions ?? []) {
1958
+ const key = `${decision.question}
1959
+ ${decision.chosen}
1960
+ ${decision.reasoning}`;
1961
+ if (!seen.has(key)) {
1962
+ seen.add(key);
1963
+ decisions.push(decision);
1964
+ }
1965
+ }
1966
+ if (decisions.length === 0) {
1967
+ return "Decisions:\n- None recorded\n";
1968
+ }
1969
+ return [
1970
+ "Decisions:",
1971
+ ...decisions.map(
1972
+ (decision) => [
1973
+ `- Question: ${decision.question}`,
1974
+ ` Chosen: ${decision.chosen}`,
1975
+ ` Reasoning: ${decision.reasoning}`
1976
+ ].join("\n")
1977
+ )
1978
+ ].join("\n").concat("\n");
1979
+ }
1980
+ function renderFindings(trajectory) {
1981
+ const findings = trajectory.chapters.flatMap(
1982
+ (chapter) => chapter.events.filter((event) => event.type === "finding").map((event) => asFinding(event.raw, event.content))
1983
+ );
1984
+ if (findings.length === 0) {
1985
+ return "Findings:\n- None recorded\n";
1986
+ }
1987
+ return [
1988
+ "Findings:",
1989
+ ...findings.map(
1990
+ (finding) => [
1991
+ `- What: ${finding.what}`,
1992
+ ` Where: ${finding.where}`,
1993
+ ` Significance: ${finding.significance}`
1994
+ ].join("\n")
1995
+ )
1996
+ ].join("\n").concat("\n");
1997
+ }
1998
+ function renderRetrospective(retrospective) {
1999
+ if (!retrospective) {
2000
+ return "Retrospective:\n- None recorded\n";
2001
+ }
2002
+ const lines = [
2003
+ "Retrospective:",
2004
+ `- Summary: ${retrospective.summary}`,
2005
+ ` Approach: ${retrospective.approach}`
2006
+ ];
2007
+ if (retrospective.challenges && retrospective.challenges.length > 0) {
2008
+ lines.push(` Challenges: ${retrospective.challenges.join("; ")}`);
2009
+ }
2010
+ if (retrospective.learnings && retrospective.learnings.length > 0) {
2011
+ lines.push(` Learnings: ${retrospective.learnings.join("; ")}`);
2012
+ }
2013
+ if (retrospective.suggestions && retrospective.suggestions.length > 0) {
2014
+ lines.push(` Suggestions: ${retrospective.suggestions.join("; ")}`);
2015
+ }
2016
+ if (retrospective.timeSpent) {
2017
+ lines.push(` Time spent: ${retrospective.timeSpent}`);
2018
+ }
2019
+ return lines.join("\n").concat("\n");
2020
+ }
2021
+ function shouldIncludeEvent(event) {
2022
+ if (event.type === "tool_call" || event.type === "tool_result") {
2023
+ return false;
2024
+ }
2025
+ return INCLUDED_SIGNIFICANCE.has(resolveSignificance(event));
2026
+ }
2027
+ function resolveSignificance(event) {
2028
+ if (event.significance) {
2029
+ return event.significance;
2030
+ }
2031
+ switch (event.type) {
2032
+ case "decision":
2033
+ case "finding":
2034
+ case "error":
2035
+ return "high";
2036
+ case "reflection":
2037
+ case "note":
2038
+ case "message_sent":
2039
+ case "message_received":
2040
+ return "medium";
2041
+ default:
2042
+ return "low";
2043
+ }
2044
+ }
2045
+ function formatEvent(event) {
2046
+ if (event.type === "decision") {
2047
+ const decision = asDecision(event.raw);
2048
+ if (decision) {
2049
+ return `[decision/${resolveSignificance(event)}] ${decision.question} -> ${decision.chosen}`;
2050
+ }
2051
+ }
2052
+ if (event.type === "finding") {
2053
+ const finding = asFinding(event.raw, event.content);
2054
+ return `[finding/${resolveSignificance(event)}] ${finding.what} @ ${finding.where}`;
2055
+ }
2056
+ return `[${event.type}/${resolveSignificance(event)}] ${event.content}`;
2057
+ }
2058
+ function asDecision(raw) {
2059
+ if (!raw || typeof raw !== "object") {
2060
+ return null;
2061
+ }
2062
+ const candidate = raw;
2063
+ if (typeof candidate.question !== "string" || typeof candidate.chosen !== "string" || typeof candidate.reasoning !== "string") {
2064
+ return null;
2065
+ }
2066
+ return {
2067
+ question: candidate.question,
2068
+ chosen: candidate.chosen,
2069
+ reasoning: candidate.reasoning,
2070
+ alternatives: Array.isArray(candidate.alternatives) ? candidate.alternatives : [],
2071
+ confidence: candidate.confidence
2072
+ };
2073
+ }
2074
+ function asFinding(raw, fallbackContent) {
2075
+ if (!raw || typeof raw !== "object") {
2076
+ return {
2077
+ what: fallbackContent,
2078
+ where: "unknown",
2079
+ significance: "Not structured",
2080
+ category: "other"
2081
+ };
2082
+ }
2083
+ const candidate = raw;
2084
+ return {
2085
+ what: typeof candidate.what === "string" && candidate.what.trim().length > 0 ? candidate.what : fallbackContent,
2086
+ where: typeof candidate.where === "string" && candidate.where.trim().length > 0 ? candidate.where : "unknown",
2087
+ significance: typeof candidate.significance === "string" && candidate.significance.trim().length > 0 ? candidate.significance : "Not structured",
2088
+ category: candidate.category ?? "other",
2089
+ suggestedAction: typeof candidate.suggestedAction === "string" ? candidate.suggestedAction : void 0,
2090
+ confidence: candidate.confidence
2091
+ };
2092
+ }
2093
+ function truncateChapters(chapters, totalChapterChars, ratio) {
2094
+ if (ratio >= 1 || totalChapterChars === 0) {
2095
+ return chapters;
2096
+ }
2097
+ let remaining = Math.floor(totalChapterChars * ratio);
2098
+ return chapters.map((chapter, index) => {
2099
+ if (remaining <= 0) {
2100
+ return "### Chapter: Truncated\n- Omitted due to token budget\n";
2101
+ }
2102
+ const proportionalTarget = index === chapters.length - 1 ? remaining : Math.floor(chapter.length * ratio);
2103
+ const allowance = Math.max(0, Math.min(chapter.length, proportionalTarget));
2104
+ remaining -= allowance;
2105
+ return truncateText(chapter, allowance);
2106
+ });
2107
+ }
2108
+ function joinSessions(sessions) {
2109
+ return sessions.map(
2110
+ (session) => [
2111
+ session.header,
2112
+ session.agents,
2113
+ ...session.chapters,
2114
+ session.decisions,
2115
+ session.findings,
2116
+ session.retrospective,
2117
+ session.filesAndCommits
2118
+ ].filter(Boolean).join("\n").trim()
2119
+ ).join("\n\n");
2120
+ }
2121
+ function truncateText(text, maxChars) {
2122
+ if (maxChars <= 0) {
2123
+ return "";
2124
+ }
2125
+ if (text.length <= maxChars) {
2126
+ return text;
2127
+ }
2128
+ if (maxChars <= 16) {
2129
+ return text.slice(0, maxChars);
2130
+ }
2131
+ return `${text.slice(0, maxChars - 16).trimEnd()}
2132
+ [truncated]
2133
+ `;
2134
+ }
2135
+ function formatList(values) {
2136
+ return values.length > 0 ? values.join(", ") : "none";
2137
+ }
2138
+ function formatDuration(startedAt, completedAt) {
2139
+ const start = new Date(startedAt).getTime();
2140
+ const end = new Date(completedAt ?? startedAt).getTime();
2141
+ const elapsedMs = Math.max(0, end - start);
2142
+ const minutes = Math.floor(elapsedMs / 6e4);
2143
+ const hours = Math.floor(minutes / 60);
2144
+ const remainingMinutes = minutes % 60;
2145
+ if (hours > 0 && remainingMinutes > 0) {
2146
+ return `${hours}h ${remainingMinutes}m`;
2147
+ }
2148
+ if (hours > 0) {
2149
+ return `${hours}h`;
2150
+ }
2151
+ if (minutes > 0) {
2152
+ return `${minutes}m`;
2153
+ }
2154
+ return completedAt ? "0m" : "ongoing";
2155
+ }
2156
+
2157
+ // src/cli/commands/compact.ts
2158
+ function registerCompactCommand(program2) {
2159
+ program2.command("compact").description(
2160
+ "Compact trajectories into a summarized form (default: uncompacted only)"
2161
+ ).option(
2162
+ "--since <date>",
2163
+ "Include trajectories since this date (ISO format or relative like '7d')"
2164
+ ).option(
2165
+ "--until <date>",
2166
+ "Include trajectories until this date (ISO format)"
2167
+ ).option("--ids <ids>", "Comma-separated list of trajectory IDs to compact").option(
2168
+ "--workflow <id>",
2169
+ "Compact trajectories with the specified workflow ID"
2170
+ ).option("--pr <number>", "Compact trajectories associated with a PR number").option(
2171
+ "--branch <name>",
2172
+ "Compact trajectories with commits not in the specified branch (e.g., main)"
2173
+ ).option(
2174
+ "--commits <shas>",
2175
+ "Comma-separated commit SHAs to match trajectories against"
2176
+ ).option("--all", "Include all trajectories, even previously compacted ones").option("--llm", "Use LLM-based compaction when a provider is available").option("--no-llm", "Disable LLM-based compaction").option("--mechanical", "Force the original mechanical compaction flow").option(
2177
+ "--focus <areas>",
2178
+ "Comma-separated focus areas to emphasize in LLM compaction"
2179
+ ).option("--markdown", "Also write a Markdown companion file").option("--no-markdown", "Skip writing a Markdown companion file").option("--dry-run", "Preview what would be compacted without saving").option("--output <path>", "Output path for compacted trajectory").action(async (options) => {
2180
+ const trajectories = await loadTrajectories(options);
2181
+ if (trajectories.length === 0) {
2182
+ if (options.all || options.since || options.ids || options.workflow || options.pr || options.branch || options.commits) {
2183
+ console.log("No trajectories found matching criteria");
2184
+ } else {
2185
+ console.log(
2186
+ "No uncompacted trajectories found. Use --all to include previously compacted."
2187
+ );
2188
+ }
2189
+ return;
2190
+ }
2191
+ console.log(`Compacting ${trajectories.length} trajectories...
2192
+ `);
2193
+ const config = getCompactionConfig();
2194
+ const provider = await resolveProvider(config);
2195
+ const useLLM = shouldUseLLM(options, provider !== null);
2196
+ const markdownEnabled = options.markdown !== false;
2197
+ const mechanicalCompacted = compactTrajectories(
2198
+ trajectories,
2199
+ options.workflow
2200
+ );
2201
+ if (!useLLM || provider === null) {
2202
+ if (options.llm && provider === null && !options.mechanical) {
2203
+ console.log(
2204
+ "No LLM provider detected; falling back to mechanical compaction.\n"
2205
+ );
2206
+ }
2207
+ if (options.dryRun) {
2208
+ console.log("=== DRY RUN - Preview ===\n");
2209
+ printCompactedSummary(mechanicalCompacted);
2210
+ return;
2211
+ }
2212
+ const outputPath2 = options.output || getDefaultOutputPath(mechanicalCompacted, options.workflow);
2213
+ saveCompactionArtifacts(
2214
+ mechanicalCompacted,
2215
+ outputPath2,
2216
+ markdownEnabled
2217
+ );
2218
+ await markTrajectoriesAsCompacted(trajectories, mechanicalCompacted.id);
2219
+ console.log(`
2220
+ Compacted trajectory saved to: ${outputPath2}`);
2221
+ if (markdownEnabled) {
2222
+ console.log(
2223
+ `Markdown summary saved to: ${getMarkdownOutputPath(outputPath2)}`
2224
+ );
2225
+ }
2226
+ printCompactedSummary(mechanicalCompacted);
2227
+ return;
2228
+ }
2229
+ const llmPlan = buildLLMCompactionPlan(
2230
+ trajectories,
2231
+ parseFocusAreas(options.focus),
2232
+ config.maxInputTokens,
2233
+ config.maxOutputTokens
2234
+ );
2235
+ console.log(
2236
+ `Using ${getProviderLabel(provider)} compaction${config.model ? ` with model ${config.model}` : ""}.`
2237
+ );
2238
+ console.log(
2239
+ `Estimated: ~${llmPlan.estimatedInputTokens} input tokens, ~${llmPlan.estimatedOutputTokens} output tokens`
2240
+ );
2241
+ if (options.dryRun) {
2242
+ printLLMDryRun(llmPlan, config.model, options.workflow);
2243
+ return;
2244
+ }
2245
+ const llmOutput = await provider.complete(llmPlan.messages, {
2246
+ maxTokens: config.maxOutputTokens,
2247
+ temperature: config.temperature,
2248
+ jsonMode: provider instanceof OpenAIProvider
2249
+ });
2250
+ const llmCompacted = parseCompactionResponse(llmOutput);
2251
+ const mergedCompaction = mergeCompactionWithMetadata(
2252
+ {
2253
+ id: mechanicalCompacted.id,
2254
+ version: mechanicalCompacted.version,
2255
+ type: mechanicalCompacted.type,
2256
+ compactedAt: mechanicalCompacted.compactedAt,
2257
+ sourceTrajectories: mechanicalCompacted.sourceTrajectories,
2258
+ dateRange: mechanicalCompacted.dateRange,
2259
+ summary: mechanicalCompacted.summary,
2260
+ filesAffected: mechanicalCompacted.filesAffected,
2261
+ commits: mechanicalCompacted.commits
2262
+ },
2263
+ llmCompacted
2264
+ );
2265
+ const compacted = {
2266
+ ...mechanicalCompacted,
2267
+ ...mergedCompaction
2268
+ };
2269
+ const outputPath = options.output || getDefaultOutputPath(compacted, options.workflow);
2270
+ saveCompactionArtifacts(compacted, outputPath, markdownEnabled);
2271
+ await markTrajectoriesAsCompacted(trajectories, compacted.id);
2272
+ console.log(`
2273
+ Compacted trajectory saved to: ${outputPath}`);
2274
+ if (markdownEnabled) {
2275
+ console.log(
2276
+ `Markdown summary saved to: ${getMarkdownOutputPath(outputPath)}`
2277
+ );
2278
+ }
2279
+ printCompactedSummary(compacted);
2280
+ });
2281
+ }
2282
+ async function loadTrajectories(options) {
2283
+ const trajectories = [];
2284
+ const targetIds = options.ids ? options.ids.split(",").map((s) => s.trim()) : null;
2285
+ const sinceDate = options.since ? parseRelativeDate(options.since) : null;
2286
+ const untilDate = options.until ? new Date(options.until) : null;
2287
+ const branchCommits = options.branch ? getBranchCommits(options.branch) : null;
2288
+ const targetCommits = options.commits ? new Set(
2289
+ options.commits.split(",").flatMap((sha) => {
2290
+ const trimmed = sha.trim();
2291
+ if (!trimmed) return [];
2292
+ return [trimmed, trimmed.slice(0, 7)];
2293
+ })
2294
+ ) : null;
2295
+ const compactedIds = options.all ? /* @__PURE__ */ new Set() : getCompactedTrajectoryIds();
2296
+ const searchPaths = getSearchPaths();
2297
+ const seenIds = /* @__PURE__ */ new Set();
2298
+ for (const searchPath of searchPaths) {
2299
+ if (!existsSync3(searchPath)) continue;
2300
+ const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
2301
+ process.env.TRAJECTORIES_DATA_DIR = searchPath;
2302
+ const storage = new FileStorage();
2303
+ if (originalDataDir !== void 0) {
2304
+ process.env.TRAJECTORIES_DATA_DIR = originalDataDir;
2305
+ } else {
2306
+ delete process.env.TRAJECTORIES_DATA_DIR;
2307
+ }
2308
+ await storage.initialize();
2309
+ const summaries = await storage.list({
2310
+ status: "completed",
2311
+ limit: Number.MAX_SAFE_INTEGER
2312
+ });
2313
+ for (const summary of summaries) {
2314
+ if (seenIds.has(summary.id)) continue;
2315
+ if (compactedIds.has(summary.id)) continue;
2316
+ if (targetIds && !targetIds.includes(summary.id)) continue;
2317
+ const startDate = new Date(summary.startedAt);
2318
+ if (sinceDate && startDate < sinceDate) continue;
2319
+ if (untilDate && startDate > untilDate) continue;
2320
+ const trajectory = await storage.get(summary.id);
2321
+ if (trajectory) {
2322
+ seenIds.add(summary.id);
2323
+ if (options.workflow && trajectory.workflowId !== options.workflow) {
2324
+ continue;
2325
+ }
2326
+ if (options.pr) {
2327
+ const escaped = options.pr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2328
+ const prPattern = new RegExp(
2329
+ `#${escaped}\\b|\\bPR\\s*#?\\s*${escaped}\\b`,
2330
+ "i"
2331
+ );
2332
+ const matchesPR = prPattern.test(trajectory.task.title) || prPattern.test(trajectory.task.description || "") || trajectory.commits.some((c) => prPattern.test(c));
2333
+ if (!matchesPR) continue;
2334
+ }
2335
+ if (branchCommits) {
2336
+ const hasMatchingCommit = trajectory.commits.some(
2337
+ (c) => branchCommits.has(c.slice(0, 7)) || branchCommits.has(c)
2338
+ );
2339
+ if (!hasMatchingCommit && trajectory.commits.length > 0) continue;
2340
+ }
2341
+ if (targetCommits) {
2342
+ const hasMatchingCommit = trajectory.commits.some(
2343
+ (c) => targetCommits.has(c) || targetCommits.has(c.slice(0, 7))
2344
+ );
2345
+ if (!hasMatchingCommit) continue;
2346
+ }
2347
+ trajectories.push(trajectory);
2348
+ }
2349
+ }
2350
+ }
2351
+ return trajectories;
2352
+ }
2353
+ function getBranchCommits(targetBranch) {
2354
+ const commits = /* @__PURE__ */ new Set();
2355
+ try {
2356
+ const output = execFileSync(
2357
+ "git",
2358
+ ["log", `${targetBranch}..HEAD`, "--format=%H"],
2359
+ {
2360
+ encoding: "utf-8",
2361
+ stdio: ["pipe", "pipe", "pipe"]
2362
+ }
2363
+ );
2364
+ for (const line of output.trim().split("\n")) {
2365
+ if (line) {
2366
+ commits.add(line);
2367
+ commits.add(line.slice(0, 7));
2368
+ }
2369
+ }
2370
+ } catch {
2371
+ console.warn(
2372
+ `Warning: Could not get commits for branch comparison with ${targetBranch}`
2373
+ );
2374
+ }
2375
+ return commits;
2376
+ }
2377
+ function getCompactedTrajectoryIds() {
2378
+ const compacted = /* @__PURE__ */ new Set();
2379
+ const searchPaths = getSearchPaths();
2380
+ for (const searchPath of searchPaths) {
2381
+ const indexPath = join4(searchPath, "index.json");
2382
+ if (!existsSync3(indexPath)) continue;
2383
+ try {
2384
+ const indexContent = readFileSync2(indexPath, "utf-8");
2385
+ const index = JSON.parse(indexContent);
2386
+ for (const [id, entry] of Object.entries(index.trajectories || {})) {
2387
+ if (entry.compactedInto) {
2388
+ compacted.add(id);
2389
+ }
2390
+ }
2391
+ } catch {
2392
+ }
2393
+ }
2394
+ return compacted;
2395
+ }
2396
+ async function markTrajectoriesAsCompacted(trajectories, compactedIntoId) {
2397
+ const searchPaths = getSearchPaths();
2398
+ for (const searchPath of searchPaths) {
2399
+ const indexPath = join4(searchPath, "index.json");
2400
+ if (!existsSync3(indexPath)) continue;
2401
+ try {
2402
+ const indexContent = readFileSync2(indexPath, "utf-8");
2403
+ const index = JSON.parse(indexContent);
2404
+ let updated = false;
2405
+ for (const traj of trajectories) {
2406
+ if (index.trajectories[traj.id]) {
2407
+ index.trajectories[traj.id].compactedInto = compactedIntoId;
2408
+ updated = true;
2409
+ }
2410
+ }
2411
+ if (updated) {
2412
+ index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
2413
+ writeFileSync(indexPath, JSON.stringify(index, null, 2));
2414
+ }
2415
+ } catch {
2416
+ }
2417
+ }
2418
+ }
2419
+ function parseRelativeDate(input) {
2420
+ const match = input.match(/^(\d+)([dwmh])$/);
2421
+ if (match) {
2422
+ const amount = Number.parseInt(match[1], 10);
2423
+ const unit = match[2];
2424
+ const now = /* @__PURE__ */ new Date();
2425
+ switch (unit) {
2426
+ case "h":
2427
+ return new Date(now.getTime() - amount * 60 * 60 * 1e3);
2428
+ case "d":
2429
+ return new Date(now.getTime() - amount * 24 * 60 * 60 * 1e3);
2430
+ case "w":
2431
+ return new Date(now.getTime() - amount * 7 * 24 * 60 * 60 * 1e3);
2432
+ case "m":
2433
+ return new Date(now.getTime() - amount * 30 * 24 * 60 * 60 * 1e3);
2434
+ }
2435
+ }
2436
+ return new Date(input);
2437
+ }
2438
+ function compactTrajectories(trajectories, workflowId) {
2439
+ const allDecisions = [];
2440
+ const allLearnings = [];
2441
+ const allFindings = [];
2442
+ const allFiles = /* @__PURE__ */ new Set();
2443
+ const allCommits = /* @__PURE__ */ new Set();
2444
+ const allAgents = /* @__PURE__ */ new Set();
2445
+ let totalEvents = 0;
2446
+ for (const traj of trajectories) {
2447
+ for (const agent of traj.agents) {
2448
+ allAgents.add(agent.name);
2449
+ }
2450
+ for (const file of traj.filesChanged) {
2451
+ allFiles.add(file);
2452
+ }
2453
+ for (const commit of traj.commits) {
2454
+ allCommits.add(commit);
2455
+ }
2456
+ for (const chapter of traj.chapters) {
2457
+ totalEvents += chapter.events.length;
2458
+ for (const event of chapter.events) {
2459
+ if (event.type === "decision" && event.raw) {
2460
+ const decision = event.raw;
2461
+ allDecisions.push({
2462
+ decision,
2463
+ fromTrajectory: traj.id,
2464
+ timestamp: event.ts
2465
+ });
2466
+ }
2467
+ if (event.type === "finding" && event.content) {
2468
+ allFindings.push(event.content);
2469
+ }
2470
+ }
2471
+ }
2472
+ if (traj.retrospective?.learnings) {
2473
+ allLearnings.push(...traj.retrospective.learnings);
2474
+ }
2475
+ if (traj.retrospective?.decisions) {
2476
+ for (const decision of traj.retrospective.decisions) {
2477
+ allDecisions.push({
2478
+ decision,
2479
+ fromTrajectory: traj.id,
2480
+ timestamp: new Date(traj.completedAt || traj.startedAt).getTime()
2481
+ });
2482
+ }
2483
+ }
2484
+ }
2485
+ const decisionGroups = groupDecisions(allDecisions);
2486
+ const uniqueLearnings = [...new Set(allLearnings)];
2487
+ const dates = trajectories.map((t) => new Date(t.startedAt).getTime());
2488
+ const minDate = new Date(Math.min(...dates));
2489
+ const maxDate = new Date(
2490
+ Math.max(
2491
+ ...trajectories.map(
2492
+ (t) => new Date(t.completedAt || t.startedAt).getTime()
2493
+ )
2494
+ )
2495
+ );
2496
+ return {
2497
+ id: `compact_${generateRandomId()}`,
2498
+ version: 1,
2499
+ type: "compacted",
2500
+ compactedAt: (/* @__PURE__ */ new Date()).toISOString(),
2501
+ workflowId,
2502
+ sourceTrajectories: trajectories.map((t) => t.id),
2503
+ dateRange: {
2504
+ start: minDate.toISOString(),
2505
+ end: maxDate.toISOString()
2506
+ },
2507
+ summary: {
2508
+ totalDecisions: allDecisions.length,
2509
+ totalEvents,
2510
+ uniqueAgents: [...allAgents]
2511
+ },
2512
+ decisionGroups,
2513
+ keyLearnings: uniqueLearnings,
2514
+ keyFindings: [...new Set(allFindings)],
2515
+ filesAffected: [...allFiles],
2516
+ commits: [...allCommits]
2517
+ };
2518
+ }
2519
+ function groupDecisions(decisions) {
2520
+ const categories = {};
2521
+ const categoryKeywords = {
2522
+ architecture: [
2523
+ "architecture",
2524
+ "structure",
2525
+ "pattern",
2526
+ "design",
2527
+ "module",
2528
+ "component"
2529
+ ],
2530
+ api: ["api", "endpoint", "rest", "graphql", "http", "request", "response"],
2531
+ database: ["database", "schema", "migration", "query", "sql", "model"],
2532
+ testing: ["test", "spec", "coverage", "assertion", "mock"],
2533
+ security: [
2534
+ "security",
2535
+ "auth",
2536
+ "permission",
2537
+ "token",
2538
+ "credential",
2539
+ "encrypt"
2540
+ ],
2541
+ performance: ["performance", "optimize", "cache", "speed", "memory"],
2542
+ tooling: ["tool", "config", "build", "lint", "format", "ci", "cd"],
2543
+ naming: ["name", "rename", "convention", "format"],
2544
+ compliance: ["spec", "standard", "compliance", "convention", "align"]
2545
+ };
2546
+ for (const { decision, fromTrajectory } of decisions) {
2547
+ const text = `${decision.question} ${decision.reasoning}`.toLowerCase();
2548
+ let matchedCategory = "other";
2549
+ for (const [category, keywords] of Object.entries(categoryKeywords)) {
2550
+ if (keywords.some((kw) => text.includes(kw))) {
2551
+ matchedCategory = category;
2552
+ break;
2553
+ }
2554
+ }
2555
+ if (!categories[matchedCategory]) {
2556
+ categories[matchedCategory] = {
2557
+ category: matchedCategory,
2558
+ decisions: []
2559
+ };
2560
+ }
2561
+ categories[matchedCategory].decisions.push({
2562
+ question: decision.question,
2563
+ chosen: decision.chosen,
2564
+ reasoning: decision.reasoning,
2565
+ fromTrajectory
2566
+ });
2567
+ }
2568
+ return Object.values(categories).sort(
2569
+ (a, b) => b.decisions.length - a.decisions.length
2570
+ );
2571
+ }
2572
+ function shouldUseLLM(options, providerAvailable) {
2573
+ if (options.mechanical) {
2574
+ return false;
2575
+ }
2576
+ if (options.llm === false) {
2577
+ return false;
2578
+ }
2579
+ if (options.llm === true) {
2580
+ return providerAvailable;
2581
+ }
2582
+ return providerAvailable;
2583
+ }
2584
+ function buildLLMCompactionPlan(trajectories, focusAreas, maxInputTokens, maxOutputTokens) {
2585
+ const serialized = serializeForLLM(trajectories, maxInputTokens);
2586
+ const messages = buildCompactionPrompt(serialized, {
2587
+ focusAreas,
2588
+ maxOutputTokens
2589
+ });
2590
+ return {
2591
+ messages,
2592
+ estimatedInputTokens: estimateTokens(
2593
+ messages.map((message) => message.content).join("\n\n")
2594
+ ),
2595
+ estimatedOutputTokens: maxOutputTokens,
2596
+ focusAreas
2597
+ };
2598
+ }
2599
+ function parseFocusAreas(focus) {
2600
+ if (!focus) {
2601
+ return [];
2602
+ }
2603
+ return focus.split(",").map((area) => area.trim()).filter(Boolean);
2604
+ }
2605
+ function estimateTokens(text) {
2606
+ return Math.max(1, Math.ceil(text.length / 4));
2607
+ }
2608
+ function printLLMDryRun(plan, model, workflowId) {
2609
+ console.log("=== DRY RUN - LLM Prompt Preview ===\n");
2610
+ console.log(
2611
+ `Estimated: ~${plan.estimatedInputTokens} input tokens, ~${plan.estimatedOutputTokens} output tokens`
2612
+ );
2613
+ if (model) {
2614
+ console.log(`Configured model: ${model}`);
2615
+ }
2616
+ if (workflowId) {
2617
+ console.log(`Workflow: ${workflowId}`);
2618
+ }
2619
+ if (plan.focusAreas.length > 0) {
2620
+ console.log(`Focus: ${plan.focusAreas.join(", ")}`);
2621
+ }
2622
+ console.log("");
2623
+ for (const message of plan.messages) {
2624
+ console.log(`[${message.role.toUpperCase()}]`);
2625
+ console.log(message.content);
2626
+ console.log("");
2627
+ }
2628
+ }
2629
+ function getProviderLabel(provider) {
2630
+ if (provider instanceof OpenAIProvider) {
2631
+ return "OpenAI";
2632
+ }
2633
+ if (provider instanceof AnthropicProvider) {
2634
+ return "Anthropic";
2635
+ }
2636
+ if (provider instanceof CLIProvider) {
2637
+ return `CLI (${provider.cliName})`;
2638
+ }
2639
+ return "LLM";
2640
+ }
2641
+ function getDefaultOutputPath(compacted, workflowId) {
2642
+ const trajDir = process.env.TRAJECTORIES_DATA_DIR || ".trajectories";
2643
+ const compactedDir = join4(trajDir, "compacted");
2644
+ if (!existsSync3(compactedDir)) {
2645
+ mkdirSync(compactedDir, { recursive: true });
2646
+ }
2647
+ if (workflowId) {
2648
+ return join4(compactedDir, `workflow-${workflowId}.json`);
2649
+ }
2650
+ const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2651
+ return join4(compactedDir, `${compacted.id}_${dateStr}.json`);
2652
+ }
2653
+ function saveCompactionArtifacts(compacted, outputPath, markdownEnabled) {
2654
+ const dir = dirname(outputPath);
2655
+ if (!existsSync3(dir)) {
2656
+ mkdirSync(dir, { recursive: true });
2657
+ }
2658
+ writeFileSync(outputPath, JSON.stringify(compacted, null, 2));
2659
+ if (markdownEnabled) {
2660
+ writeFileSync(
2661
+ getMarkdownOutputPath(outputPath),
2662
+ renderCompactionMarkdown(compacted)
2663
+ );
2664
+ }
2665
+ }
2666
+ function getMarkdownOutputPath(outputPath) {
2667
+ return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".md") : `${outputPath}.md`;
2668
+ }
2669
+ function renderCompactionMarkdown(compacted) {
2670
+ if (compacted.narrative) {
2671
+ return generateCompactionMarkdown(
2672
+ compacted
2673
+ );
2674
+ }
2675
+ const decisionGroups = compacted.decisionGroups.length > 0 ? compacted.decisionGroups.map((group) => {
2676
+ const decisions = group.decisions.length > 0 ? group.decisions.map(
2677
+ (decision) => `- ${decision.question} -> ${decision.chosen} (${decision.fromTrajectory})`
2678
+ ).join("\n") : "- None";
2679
+ return `## ${capitalize(group.category)}
2680
+ ${decisions}`;
2681
+ }).join("\n\n") : "## Decision Groups\n- None";
2682
+ const learnings = compacted.keyLearnings.length > 0 ? compacted.keyLearnings.map((learning) => `- ${learning}`).join("\n") : "- None";
2683
+ const findings = compacted.keyFindings.length > 0 ? compacted.keyFindings.map((finding) => `- ${finding}`).join("\n") : "- None";
2684
+ return [
2685
+ `# Trajectory Compaction: ${formatDate3(compacted.dateRange.start)} - ${formatDate3(compacted.dateRange.end)}`,
2686
+ "",
2687
+ "## Summary",
2688
+ `- Sessions: ${compacted.sourceTrajectories.length}`,
2689
+ ...compacted.workflowId ? [`- Workflow: ${compacted.workflowId}`] : [],
2690
+ `- Decisions: ${compacted.summary.totalDecisions}`,
2691
+ `- Events: ${compacted.summary.totalEvents}`,
2692
+ `- Agents: ${compacted.summary.uniqueAgents.join(", ") || "None"}`,
2693
+ `- Files: ${compacted.filesAffected.length}`,
2694
+ `- Commits: ${compacted.commits.length}`,
2695
+ "",
2696
+ decisionGroups,
2697
+ "",
2698
+ "## Key Learnings",
2699
+ learnings,
2700
+ "",
2701
+ "## Key Findings",
2702
+ findings
2703
+ ].join("\n");
2704
+ }
2705
+ function printCompactedSummary(compacted) {
2706
+ console.log("=== Compacted Trajectory Summary ===\n");
2707
+ console.log(`ID: ${compacted.id}`);
2708
+ if (compacted.workflowId) {
2709
+ console.log(`Workflow: ${compacted.workflowId}`);
2710
+ }
2711
+ console.log(`Source trajectories: ${compacted.sourceTrajectories.length}`);
2712
+ console.log(
2713
+ `Date range: ${formatDate3(compacted.dateRange.start)} - ${formatDate3(compacted.dateRange.end)}`
2714
+ );
2715
+ console.log(`Total decisions: ${compacted.summary.totalDecisions}`);
2716
+ console.log(`Total events: ${compacted.summary.totalEvents}`);
2717
+ console.log(`Agents: ${compacted.summary.uniqueAgents.join(", ")}`);
2718
+ console.log("");
2719
+ if (compacted.narrative) {
2720
+ console.log("=== Narrative ===\n");
2721
+ console.log(compacted.narrative);
2722
+ console.log("");
2723
+ if (compacted.decisions && compacted.decisions.length > 0) {
2724
+ console.log("=== Key Decisions ===\n");
2725
+ for (const decision of compacted.decisions.slice(0, 5)) {
2726
+ console.log(` - ${decision.question}`);
2727
+ console.log(` Chosen: ${decision.chosen}`);
2728
+ if (decision.impact) {
2729
+ console.log(` Impact: ${decision.impact}`);
2730
+ }
2731
+ }
2732
+ if (compacted.decisions.length > 5) {
2733
+ console.log(` ... and ${compacted.decisions.length - 5} more`);
2734
+ }
2735
+ console.log("");
2736
+ }
2737
+ if (compacted.openQuestions && compacted.openQuestions.length > 0) {
2738
+ console.log("=== Open Questions ===\n");
2739
+ for (const question of compacted.openQuestions.slice(0, 5)) {
2740
+ console.log(` - ${question}`);
2741
+ }
2742
+ if (compacted.openQuestions.length > 5) {
2743
+ console.log(` ... and ${compacted.openQuestions.length - 5} more`);
2744
+ }
2745
+ console.log("");
2746
+ }
2747
+ } else {
2748
+ console.log("=== Decision Groups ===\n");
2749
+ for (const group of compacted.decisionGroups) {
2750
+ console.log(
2751
+ `${capitalize(group.category)} (${group.decisions.length} decisions):`
2752
+ );
2753
+ for (const decision of group.decisions.slice(0, 3)) {
2754
+ console.log(` - ${decision.question}`);
2755
+ console.log(` Chose: ${decision.chosen}`);
2756
+ }
2757
+ if (group.decisions.length > 3) {
2758
+ console.log(` ... and ${group.decisions.length - 3} more`);
2759
+ }
2760
+ console.log("");
2761
+ }
2762
+ if (compacted.keyLearnings.length > 0) {
2763
+ console.log("=== Key Learnings ===\n");
2764
+ for (const learning of compacted.keyLearnings.slice(0, 5)) {
2765
+ console.log(` - ${learning}`);
2766
+ }
2767
+ if (compacted.keyLearnings.length > 5) {
2768
+ console.log(` ... and ${compacted.keyLearnings.length - 5} more`);
2769
+ }
2770
+ console.log("");
2771
+ }
2772
+ }
2773
+ if (compacted.filesAffected.length > 0) {
2774
+ console.log(`Files affected: ${compacted.filesAffected.length}`);
2775
+ }
2776
+ if (compacted.commits.length > 0) {
2777
+ console.log(`Commits: ${compacted.commits.length}`);
2778
+ }
2779
+ }
2780
+ function formatDate3(isoString) {
2781
+ return new Date(isoString).toLocaleDateString("en-US", {
2782
+ month: "short",
2783
+ day: "numeric",
2784
+ year: "numeric"
2785
+ });
2786
+ }
2787
+ function capitalize(str) {
2788
+ return str.charAt(0).toUpperCase() + str.slice(1);
2789
+ }
2790
+
2791
+ // src/cli/commands/complete.ts
2792
+ import { existsSync as existsSync4 } from "fs";
2793
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
2794
+ import { join as join5 } from "path";
2795
+
2796
+ // src/core/trace.ts
2797
+ import { execSync } from "child_process";
2798
+ import { createHash } from "crypto";
2799
+ function isValidGitRef(ref) {
2800
+ const validRefPattern = /^[a-zA-Z0-9_\-./]+$/;
2801
+ const commitHashPattern = /^[a-fA-F0-9]{7,40}$/;
2802
+ if (ref === "HEAD" || ref === "working") {
2803
+ return true;
2804
+ }
2805
+ if (commitHashPattern.test(ref)) {
2806
+ return true;
2807
+ }
2808
+ if (validRefPattern.test(ref) && ref.length <= 255) {
2809
+ if (!ref.includes("..") || ref.split("..").every((p) => p.length > 0)) {
2810
+ return true;
2811
+ }
2812
+ }
2813
+ return false;
2814
+ }
2815
+ function isGitRepo() {
2816
+ try {
2817
+ execSync("git rev-parse --is-inside-work-tree", {
2818
+ encoding: "utf-8",
2819
+ stdio: ["pipe", "pipe", "pipe"]
2820
+ });
2821
+ return true;
2822
+ } catch {
2823
+ return false;
2824
+ }
2825
+ }
2826
+ function getGitHead() {
2827
+ if (!isGitRepo()) {
2828
+ return null;
2829
+ }
2830
+ try {
2831
+ const head = execSync("git rev-parse HEAD", {
2832
+ encoding: "utf-8",
2833
+ stdio: ["pipe", "pipe", "pipe"]
2834
+ }).trim();
2835
+ return head;
2836
+ } catch {
2837
+ return null;
2838
+ }
2839
+ }
2840
+ function captureGitState() {
2841
+ return getGitHead();
2842
+ }
2843
+ function parseDiffOutput(diffOutput) {
2844
+ const files = [];
2845
+ const lines = diffOutput.split("\n");
2846
+ let currentFile = null;
2847
+ let currentRanges = [];
2848
+ for (const line of lines) {
2849
+ const diffHeaderMatch = line.match(/^diff --git a\/.+ b\/(.+)$/);
2850
+ if (diffHeaderMatch) {
2851
+ if (currentFile) {
2852
+ files.push({ path: currentFile, ranges: currentRanges });
2853
+ }
2854
+ currentFile = diffHeaderMatch[1];
2855
+ currentRanges = [];
2856
+ continue;
2857
+ }
2858
+ const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
2859
+ if (hunkMatch && currentFile) {
2860
+ const startLine = Number.parseInt(hunkMatch[1], 10);
2861
+ const lineCount = hunkMatch[2] ? Number.parseInt(hunkMatch[2], 10) : 1;
2862
+ if (lineCount > 0) {
2863
+ currentRanges.push({
2864
+ start_line: startLine,
2865
+ end_line: startLine + lineCount - 1
2866
+ });
2867
+ }
2868
+ }
2869
+ }
2870
+ if (currentFile) {
2871
+ files.push({ path: currentFile, ranges: currentRanges });
2872
+ }
2873
+ return files;
2874
+ }
2875
+ function getChangedFiles(startRef, endRef = "HEAD") {
2876
+ if (!isGitRepo()) {
2877
+ return [];
2878
+ }
2879
+ if (!isValidGitRef(startRef) || !isValidGitRef(endRef)) {
2880
+ return [];
2881
+ }
2882
+ try {
2883
+ const diffOutput = execSync(`git diff ${startRef}..${endRef}`, {
2884
+ encoding: "utf-8",
2885
+ stdio: ["pipe", "pipe", "pipe"],
2886
+ maxBuffer: 10 * 1024 * 1024
2887
+ // 10MB buffer for large diffs
2888
+ });
2889
+ return parseDiffOutput(diffOutput);
2890
+ } catch {
2891
+ return [];
2892
+ }
2893
+ }
2894
+ function detectModel() {
2895
+ if (process.env.TRAIL_TRACE_MODEL) {
2896
+ return process.env.TRAIL_TRACE_MODEL;
2897
+ }
2898
+ if (process.env.ANTHROPIC_MODEL) {
2899
+ const model = process.env.ANTHROPIC_MODEL;
2900
+ return model.includes("/") ? model : `anthropic/${model}`;
2901
+ }
2902
+ if (process.env.OPENAI_MODEL) {
2903
+ const model = process.env.OPENAI_MODEL;
2904
+ return model.includes("/") ? model : `openai/${model}`;
2905
+ }
2906
+ return "unknown";
2907
+ }
2908
+ function generateTraceId() {
2909
+ return crypto.randomUUID();
2910
+ }
2911
+ function generateTrace(trajectory, startRef) {
2912
+ if (!isGitRepo()) {
2913
+ return null;
2914
+ }
2915
+ const endRef = getGitHead();
2916
+ if (!endRef) {
2917
+ return null;
2918
+ }
2919
+ const changedFiles = getChangedFiles(startRef, endRef);
2920
+ if (changedFiles.length === 0) {
2921
+ return null;
2922
+ }
2923
+ const model = detectModel();
2924
+ const traceFiles = changedFiles.map(({ path: path2, ranges }) => ({
2925
+ path: path2,
2926
+ conversations: [
2927
+ {
2928
+ contributor: {
2929
+ type: "ai",
2930
+ ...model !== "unknown" ? { model_id: model } : {}
2931
+ },
2932
+ ranges: ranges.map((range) => ({
2933
+ ...range,
2934
+ revision: endRef
2935
+ }))
2936
+ }
2937
+ ]
2938
+ }));
2939
+ return {
2940
+ version: "1.0.0",
2941
+ id: generateTraceId(),
2942
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2943
+ trajectory: trajectory.id,
2944
+ files: traceFiles
2945
+ };
2946
+ }
2947
+ function migrateTraceRecord(raw) {
2948
+ const data = raw;
2949
+ let migrated = false;
2950
+ if (typeof data.version === "number") {
2951
+ data.version = "1.0.0";
2952
+ migrated = true;
2953
+ }
2954
+ if (Array.isArray(data.files)) {
2955
+ for (const file of data.files) {
2956
+ if (Array.isArray(file.conversations)) {
2957
+ for (const conv of file.conversations) {
2958
+ const contributor = conv.contributor;
2959
+ if (!contributor) continue;
2960
+ if (contributor.type === "agent") {
2961
+ contributor.type = "ai";
2962
+ migrated = true;
2963
+ }
2964
+ if ("model" in contributor) {
2965
+ const modelValue = contributor.model;
2966
+ contributor.model = void 0;
2967
+ if (modelValue && modelValue !== "unknown") {
2968
+ contributor.model_id = modelValue;
2969
+ }
2970
+ migrated = true;
2971
+ }
2972
+ }
2973
+ }
2974
+ }
2975
+ }
2976
+ return { record: data, migrated };
2977
+ }
2978
+ function createTraceRef(startRef, traceId) {
2979
+ const endRef = getGitHead();
2980
+ return {
2981
+ startRef,
2982
+ endRef: endRef ?? void 0,
2983
+ traceId
2984
+ };
2985
+ }
2986
+
2987
+ // src/core/trailers.ts
2988
+ import { execSync as execSync2 } from "child_process";
2989
+ import { readFileSync as readFileSync3 } from "fs";
2990
+ function getCommitsBetween(startRef, endRef = "HEAD") {
2991
+ if (!isGitRepo()) {
2992
+ return [];
2993
+ }
2994
+ if (!isValidGitRef(startRef) || !isValidGitRef(endRef)) {
2995
+ return [];
2996
+ }
2997
+ try {
2998
+ const output = execSync2(
2999
+ `git log --format=%H%n%h%n%s%n%an%n%aI%n--- ${startRef}..${endRef}`,
3000
+ {
3001
+ encoding: "utf-8",
3002
+ stdio: ["pipe", "pipe", "pipe"]
3003
+ }
3004
+ );
3005
+ if (!output.trim()) {
3006
+ return [];
3007
+ }
3008
+ const commits = [];
3009
+ const entries = output.trim().split("\n---\n");
3010
+ for (const entry of entries) {
3011
+ const lines = entry.trim().split("\n");
3012
+ if (lines.length >= 5) {
3013
+ commits.push({
3014
+ fullHash: lines[0],
3015
+ hash: lines[1],
3016
+ subject: lines[2],
3017
+ author: lines[3],
3018
+ date: lines[4]
3019
+ });
3020
+ }
3021
+ }
3022
+ return commits;
3023
+ } catch {
3024
+ return [];
3025
+ }
3026
+ }
3027
+ function getFilesChangedBetween(startRef, endRef = "HEAD") {
3028
+ if (!isGitRepo()) {
3029
+ return [];
3030
+ }
3031
+ if (!isValidGitRef(startRef) || !isValidGitRef(endRef)) {
3032
+ return [];
3033
+ }
3034
+ try {
3035
+ const output = execSync2(`git diff --name-only ${startRef}..${endRef}`, {
3036
+ encoding: "utf-8",
3037
+ stdio: ["pipe", "pipe", "pipe"]
3038
+ });
3039
+ return output.trim().split("\n").filter(Boolean);
3040
+ } catch {
3041
+ return [];
3042
+ }
3043
+ }
3044
+ function generateHookScript() {
3045
+ return `#!/bin/sh
3046
+ # Added by agent-trajectories - appends Trajectory trailer to commits
3047
+ # This hook reads the active trajectory and links it to your commit.
3048
+
3049
+ COMMIT_MSG_FILE="$1"
3050
+ COMMIT_SOURCE="$2"
3051
+
3052
+ # Skip for merge, squash, and amend commits
3053
+ if [ "$COMMIT_SOURCE" = "merge" ] || [ "$COMMIT_SOURCE" = "squash" ] || [ "$COMMIT_SOURCE" = "commit" ]; then
3054
+ exit 0
3055
+ fi
3056
+
3057
+ # Find the trajectories data directory
3058
+ TRAJ_DIR="\${TRAJECTORIES_DATA_DIR:-$(git rev-parse --show-toplevel)/.trajectories}"
3059
+ ACTIVE_DIR="$TRAJ_DIR/active"
3060
+
3061
+ # Check if there's an active trajectory
3062
+ if [ ! -d "$ACTIVE_DIR" ]; then
3063
+ exit 0
3064
+ fi
3065
+
3066
+ # Find the most recent active trajectory file
3067
+ ACTIVE_FILE=$(ls -t "$ACTIVE_DIR"/*.json 2>/dev/null | head -1)
3068
+ if [ -z "$ACTIVE_FILE" ]; then
3069
+ exit 0
3070
+ fi
3071
+
3072
+ # Extract trajectory ID (grep for the "id" field). Character class must
3073
+ # include underscore to match legacy traj_<timestamp>_<hex> ids -- without
3074
+ # it, grep -o silently truncates at the first internal underscore and
3075
+ # emits a wrong (shorter) id into the commit trailer.
3076
+ TRAJ_ID=$(grep -o '"id"[[:space:]]*:[[:space:]]*"traj_[a-z0-9_]*"' "$ACTIVE_FILE" | head -1 | grep -o 'traj_[a-z0-9_]*')
3077
+ if [ -z "$TRAJ_ID" ]; then
3078
+ exit 0
3079
+ fi
3080
+
3081
+ # Check if trailer already exists in the message
3082
+ if grep -q "^Trajectory: " "$COMMIT_MSG_FILE" 2>/dev/null; then
3083
+ exit 0
3084
+ fi
3085
+
3086
+ # Append the trailer with a blank line separator
3087
+ echo "" >> "$COMMIT_MSG_FILE"
3088
+ echo "Trajectory: $TRAJ_ID" >> "$COMMIT_MSG_FILE"
3089
+ `;
3090
+ }
3091
+ function detectExistingHook() {
3092
+ if (!isGitRepo()) {
3093
+ return "none";
3094
+ }
3095
+ try {
3096
+ const hooksDir = execSync2("git rev-parse --git-dir", {
3097
+ encoding: "utf-8",
3098
+ stdio: ["pipe", "pipe", "pipe"]
3099
+ }).trim();
3100
+ const hookPath = `${hooksDir}/hooks/prepare-commit-msg`;
3101
+ try {
3102
+ const content = readFileSync3(hookPath, "utf-8");
3103
+ if (content.includes("agent-trajectories")) {
3104
+ return "ours";
3105
+ }
3106
+ return "other";
3107
+ } catch {
3108
+ return "none";
3109
+ }
3110
+ } catch {
3111
+ return "none";
3112
+ }
3113
+ }
3114
+
3115
+ // src/cli/commands/complete.ts
3116
+ async function saveTraceFile(trajectory, trace) {
3117
+ const dataDir = process.env.TRAJECTORIES_DATA_DIR;
3118
+ const baseDir = dataDir ? dataDir : join5(process.cwd(), ".trajectories");
3119
+ const completedDir = join5(baseDir, "completed");
3120
+ const date = new Date(trajectory.completedAt ?? trajectory.startedAt);
3121
+ const monthDir = join5(
3122
+ completedDir,
3123
+ `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`
3124
+ );
3125
+ if (!existsSync4(monthDir)) {
3126
+ await mkdir2(monthDir, { recursive: true });
3127
+ }
3128
+ const tracePath = join5(monthDir, `${trajectory.id}.trace.json`);
3129
+ await writeFile2(tracePath, JSON.stringify(trace, null, 2), "utf-8");
3130
+ }
3131
+ function registerCompleteCommand(program2) {
3132
+ program2.command("complete").description("Complete the active trajectory with retrospective").option("--summary <text>", "Summary of what was accomplished").option("--approach <text>", "How the work was approached").option("--confidence <number>", "Confidence level 0-1", Number.parseFloat).action(async (options) => {
3133
+ const storage = new FileStorage();
3134
+ await storage.initialize();
3135
+ const active = await storage.getActive();
3136
+ if (!active) {
3137
+ console.error("Error: No active trajectory");
3138
+ console.error('Start one with: trail start "Task description"');
3139
+ throw new Error("No active trajectory");
3140
+ }
3141
+ if (!options.summary) {
3142
+ console.error("Error: --summary is required");
3143
+ throw new Error("Summary required");
3144
+ }
3145
+ const confidence = options.confidence ?? 0.8;
3146
+ if (confidence < 0 || confidence > 1) {
3147
+ console.error("Error: --confidence must be between 0 and 1");
3148
+ throw new Error("Invalid confidence");
3149
+ }
3150
+ let completed = completeTrajectory(active, {
3151
+ summary: options.summary,
3152
+ approach: options.approach || "Standard approach",
3153
+ confidence
3154
+ });
3155
+ let trace = null;
3156
+ if (active._trace?.startRef) {
3157
+ trace = generateTrace(completed, active._trace.startRef);
3158
+ if (trace) {
3159
+ const endRef = getGitHead();
3160
+ completed = {
3161
+ ...completed,
3162
+ _trace: {
3163
+ ...completed._trace,
3164
+ startRef: active._trace.startRef,
3165
+ endRef: endRef ?? void 0,
3166
+ traceId: trace.id
3167
+ }
3168
+ };
3169
+ }
3170
+ }
3171
+ if (active._trace?.startRef) {
3172
+ const commits = getCommitsBetween(active._trace.startRef);
3173
+ const filesChanged = getFilesChangedBetween(active._trace.startRef);
3174
+ if (commits.length > 0 || filesChanged.length > 0) {
3175
+ completed = {
3176
+ ...completed,
3177
+ commits: commits.map((c) => c.hash),
3178
+ filesChanged
3179
+ };
3180
+ }
3181
+ }
3182
+ await storage.save(completed);
3183
+ if (trace) {
3184
+ await saveTraceFile(completed, trace);
3185
+ }
3186
+ console.log(`\u2713 Trajectory completed: ${completed.id}`);
3187
+ console.log(` Summary: ${options.summary}`);
3188
+ console.log(` Confidence: ${Math.round(confidence * 100)}%`);
3189
+ if (completed.commits.length > 0) {
3190
+ console.log(` Commits: ${completed.commits.length}`);
3191
+ }
3192
+ if (completed.filesChanged.length > 0) {
3193
+ console.log(` Files changed: ${completed.filesChanged.length}`);
3194
+ }
3195
+ if (trace) {
3196
+ console.log(` Trace: ${trace.id} (${trace.files.length} files)`);
3197
+ }
3198
+ });
3199
+ }
3200
+
3201
+ // src/cli/commands/decision.ts
3202
+ function registerDecisionCommand(program2) {
3203
+ program2.command("decision <choice>").description("Record a decision").option(
3204
+ "-r, --reasoning <text>",
3205
+ "Why this choice was made (optional for minor decisions)"
3206
+ ).option(
3207
+ "-a, --alternatives <items>",
3208
+ "Comma-separated alternatives considered"
3209
+ ).action(async (choice, options) => {
3210
+ const storage = new FileStorage();
3211
+ await storage.initialize();
3212
+ const active = await storage.getActive();
3213
+ if (!active) {
3214
+ console.error("Error: No active trajectory");
3215
+ console.error('Start one with: trail start "Task description"');
3216
+ throw new Error("No active trajectory");
3217
+ }
3218
+ const alternatives = options.alternatives ? options.alternatives.split(",").map((s) => ({ option: s.trim(), reason: "" })) : [];
3219
+ const reasoning = options.reasoning || "";
3220
+ const updated = addDecision(active, {
3221
+ question: choice,
3222
+ chosen: choice,
3223
+ alternatives,
3224
+ reasoning
3225
+ });
3226
+ await storage.save(updated);
3227
+ console.log(`\u2713 Decision recorded: ${choice}`);
3228
+ if (reasoning) {
3229
+ console.log(` Reasoning: ${reasoning}`);
3230
+ }
3231
+ if (alternatives.length > 0) {
3232
+ const altStrings = alternatives.map(
3233
+ (a) => a.option
3234
+ );
3235
+ console.log(` Alternatives: ${altStrings.join(", ")}`);
3236
+ }
3237
+ });
3238
+ }
3239
+
3240
+ // src/cli/commands/enable.ts
3241
+ import { execSync as execSync3 } from "child_process";
3242
+ import { existsSync as existsSync5 } from "fs";
3243
+ import { chmod, mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
3244
+ import { join as join6 } from "path";
3245
+ function getHooksDir() {
3246
+ try {
3247
+ const gitDir = execSync3("git rev-parse --git-dir", {
3248
+ encoding: "utf-8",
3249
+ stdio: ["pipe", "pipe", "pipe"]
3250
+ }).trim();
3251
+ return join6(gitDir, "hooks");
3252
+ } catch {
3253
+ return null;
3254
+ }
3255
+ }
3256
+ function registerEnableCommand(program2) {
3257
+ program2.command("enable").description(
3258
+ "Install git hook to automatically link commits to trajectories"
3259
+ ).option("--force", "Overwrite existing prepare-commit-msg hook").action(async (options) => {
3260
+ if (!isGitRepo()) {
3261
+ console.error("Error: Not inside a git repository");
3262
+ throw new Error("Not a git repository");
3263
+ }
3264
+ const hooksDir = getHooksDir();
3265
+ if (!hooksDir) {
3266
+ console.error("Error: Could not determine git hooks directory");
3267
+ throw new Error("Cannot find hooks directory");
3268
+ }
3269
+ const hookPath = join6(hooksDir, "prepare-commit-msg");
3270
+ const existing = detectExistingHook();
3271
+ if (existing === "other" && !options.force) {
3272
+ console.error("Error: A prepare-commit-msg hook already exists");
3273
+ console.error(
3274
+ "Use --force to overwrite, or manually add the trailer logic"
3275
+ );
3276
+ throw new Error("Hook already exists");
3277
+ }
3278
+ if (existing === "ours") {
3279
+ console.log("Trajectory hook is already installed");
3280
+ return;
3281
+ }
3282
+ if (!existsSync5(hooksDir)) {
3283
+ await mkdir3(hooksDir, { recursive: true });
3284
+ }
3285
+ const hookContent = generateHookScript();
3286
+ await writeFile3(hookPath, hookContent, "utf-8");
3287
+ await chmod(hookPath, 493);
3288
+ console.log("Trajectory hook installed");
3289
+ console.log(` Hook: ${hookPath}`);
3290
+ console.log(
3291
+ " Commits will now include a Trajectory trailer when a trajectory is active"
3292
+ );
3293
+ });
3294
+ program2.command("disable").description("Remove the trajectory git hook").action(async () => {
3295
+ if (!isGitRepo()) {
3296
+ console.error("Error: Not inside a git repository");
3297
+ throw new Error("Not a git repository");
3298
+ }
3299
+ const hooksDir = getHooksDir();
3300
+ if (!hooksDir) {
3301
+ console.error("Error: Could not determine git hooks directory");
3302
+ throw new Error("Cannot find hooks directory");
3303
+ }
3304
+ const hookPath = join6(hooksDir, "prepare-commit-msg");
3305
+ const existing = detectExistingHook();
3306
+ if (existing === "none") {
3307
+ console.log("No trajectory hook installed");
3308
+ return;
3309
+ }
3310
+ if (existing === "other") {
3311
+ console.error(
3312
+ "Error: The prepare-commit-msg hook was not installed by agent-trajectories"
3313
+ );
3314
+ console.error("Remove it manually if needed");
3315
+ throw new Error("Hook not ours");
3316
+ }
3317
+ const { unlink: unlink2 } = await import("fs/promises");
3318
+ await unlink2(hookPath);
3319
+ console.log("Trajectory hook removed");
3320
+ });
3321
+ }
3322
+
3323
+ // src/cli/commands/export.ts
3324
+ import { exec } from "child_process";
3325
+ import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
3326
+ import { join as join7 } from "path";
3327
+
3328
+ // src/export/json.ts
3329
+ function exportToJSON(trajectory, options) {
3330
+ if (options?.compact) {
3331
+ return JSON.stringify(trajectory);
3332
+ }
3333
+ return JSON.stringify(trajectory, null, 2);
3334
+ }
3335
+
3336
+ // src/export/timeline.ts
3337
+ function exportToTimeline(trajectory) {
3338
+ const lines = [];
3339
+ lines.push(
3340
+ `\u25CF ${formatTime(trajectory.startedAt)} Started: ${trajectory.task.title}`
3341
+ );
3342
+ lines.push("\u2502");
3343
+ for (const chapter of trajectory.chapters) {
3344
+ lines.push(
3345
+ `\u251C\u2500 ${formatTime(chapter.startedAt)} Chapter: ${chapter.title}`
3346
+ );
3347
+ lines.push(`\u2502 Agent: ${chapter.agentName}`);
3348
+ lines.push("\u2502");
3349
+ for (const event of chapter.events) {
3350
+ const prefix = event.type === "decision" ? "\u251C\u2500 Decision: " : "\u251C\u2500 ";
3351
+ const timeStr = formatTime(new Date(event.ts).toISOString());
3352
+ if (event.type === "decision") {
3353
+ lines.push(`\u2502 ${timeStr} ${prefix}${event.content}`);
3354
+ } else if (event.significance === "high" || event.significance === "critical") {
3355
+ lines.push(`\u2502 ${timeStr} ${prefix}${event.content}`);
3356
+ }
3357
+ }
3358
+ if (chapter.endedAt) {
3359
+ lines.push("\u2502");
3360
+ }
3361
+ }
3362
+ if (trajectory.completedAt) {
3363
+ const status = trajectory.status === "completed" ? "Completed" : "Abandoned";
3364
+ lines.push(`\u25CB ${formatTime(trajectory.completedAt)} ${status}`);
3365
+ if (trajectory.retrospective) {
3366
+ lines.push("");
3367
+ lines.push(` Summary: ${trajectory.retrospective.summary}`);
3368
+ lines.push(
3369
+ ` Confidence: ${Math.round(trajectory.retrospective.confidence * 100)}%`
3370
+ );
3371
+ }
3372
+ }
3373
+ return lines.join("\n");
3374
+ }
3375
+ function formatTime(isoString) {
3376
+ const date = new Date(isoString);
3377
+ return date.toLocaleTimeString("en-US", {
3378
+ hour: "2-digit",
3379
+ minute: "2-digit"
3380
+ });
3381
+ }
3382
+
3383
+ // src/web/styles.ts
3384
+ var styles = `
3385
+ :root {
3386
+ --bg: #ffffff;
3387
+ --bg-secondary: #f8f9fa;
3388
+ --text: #1a1a2e;
3389
+ --text-muted: #6c757d;
3390
+ --border: #e9ecef;
3391
+ --accent: #4f46e5;
3392
+ --accent-light: #eef2ff;
3393
+ --success: #10b981;
3394
+ --warning: #f59e0b;
3395
+ --error: #ef4444;
3396
+ }
3397
+
3398
+ @media (prefers-color-scheme: dark) {
3399
+ :root {
3400
+ --bg: #1a1a2e;
3401
+ --bg-secondary: #16213e;
3402
+ --text: #e9ecef;
3403
+ --text-muted: #adb5bd;
3404
+ --border: #2d3748;
3405
+ --accent: #818cf8;
3406
+ --accent-light: #1e1b4b;
3407
+ }
3408
+ }
3409
+
3410
+ * {
3411
+ box-sizing: border-box;
3412
+ margin: 0;
3413
+ padding: 0;
3414
+ }
3415
+
3416
+ body {
3417
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
3418
+ background: var(--bg);
3419
+ color: var(--text);
3420
+ line-height: 1.6;
3421
+ padding: 2rem;
3422
+ max-width: 900px;
3423
+ margin: 0 auto;
3424
+ }
3425
+
3426
+ h1, h2, h3 {
3427
+ margin-bottom: 0.5rem;
3428
+ }
3429
+
3430
+ h1 {
3431
+ font-size: 1.75rem;
3432
+ display: flex;
3433
+ align-items: center;
3434
+ gap: 0.5rem;
3435
+ }
3436
+
3437
+ h2 {
3438
+ font-size: 1.25rem;
3439
+ color: var(--text-muted);
3440
+ border-bottom: 1px solid var(--border);
3441
+ padding-bottom: 0.5rem;
3442
+ margin-top: 1.5rem;
3443
+ }
3444
+
3445
+ .header {
3446
+ border-bottom: 2px solid var(--border);
3447
+ padding-bottom: 1rem;
3448
+ margin-bottom: 1.5rem;
3449
+ }
3450
+
3451
+ .meta {
3452
+ display: grid;
3453
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
3454
+ gap: 1rem;
3455
+ margin: 1rem 0;
3456
+ padding: 1rem;
3457
+ background: var(--bg-secondary);
3458
+ border-radius: 8px;
3459
+ }
3460
+
3461
+ .meta-item {
3462
+ display: flex;
3463
+ flex-direction: column;
3464
+ }
3465
+
3466
+ .meta-label {
3467
+ font-size: 0.75rem;
3468
+ text-transform: uppercase;
3469
+ color: var(--text-muted);
3470
+ letter-spacing: 0.05em;
3471
+ }
3472
+
3473
+ .meta-value {
3474
+ font-weight: 500;
3475
+ }
3476
+
3477
+ .status {
3478
+ display: inline-flex;
3479
+ align-items: center;
3480
+ gap: 0.25rem;
3481
+ padding: 0.25rem 0.5rem;
3482
+ border-radius: 4px;
3483
+ font-size: 0.875rem;
3484
+ font-weight: 500;
3485
+ }
3486
+
3487
+ .status-active {
3488
+ background: var(--accent-light);
3489
+ color: var(--accent);
3490
+ }
3491
+
3492
+ .status-completed {
3493
+ background: #d1fae5;
3494
+ color: #065f46;
3495
+ }
3496
+
3497
+ .status-abandoned {
3498
+ background: #fee2e2;
3499
+ color: #991b1b;
3500
+ }
3501
+
3502
+ .section {
3503
+ margin: 1.5rem 0;
3504
+ }
3505
+
3506
+ .collapsible {
3507
+ cursor: pointer;
3508
+ user-select: none;
3509
+ }
3510
+
3511
+ .collapsible::before {
3512
+ content: '\u25B8 ';
3513
+ display: inline-block;
3514
+ transition: transform 0.2s;
3515
+ }
3516
+
3517
+ .collapsible.open::before {
3518
+ transform: rotate(90deg);
3519
+ }
3520
+
3521
+ .collapsible-content {
3522
+ display: none;
3523
+ margin-top: 0.5rem;
3524
+ padding-left: 1rem;
3525
+ border-left: 2px solid var(--border);
3526
+ }
3527
+
3528
+ .collapsible.open + .collapsible-content {
3529
+ display: block;
3530
+ }
3531
+
3532
+ .decision {
3533
+ background: var(--bg-secondary);
3534
+ border-radius: 8px;
3535
+ padding: 1rem;
3536
+ margin: 0.75rem 0;
3537
+ border-left: 3px solid var(--accent);
3538
+ }
3539
+
3540
+ .decision-title {
3541
+ font-weight: 600;
3542
+ margin-bottom: 0.5rem;
3543
+ }
3544
+
3545
+ .decision-reasoning {
3546
+ color: var(--text-muted);
3547
+ font-size: 0.9rem;
3548
+ }
3549
+
3550
+ .alternatives {
3551
+ margin-top: 0.5rem;
3552
+ font-size: 0.85rem;
3553
+ }
3554
+
3555
+ .alternatives-label {
3556
+ color: var(--text-muted);
3557
+ }
3558
+
3559
+ .timeline {
3560
+ position: relative;
3561
+ padding-left: 1.5rem;
3562
+ }
3563
+
3564
+ .timeline::before {
3565
+ content: '';
3566
+ position: absolute;
3567
+ left: 0.35rem;
3568
+ top: 0;
3569
+ bottom: 0;
3570
+ width: 2px;
3571
+ background: var(--border);
3572
+ }
3573
+
3574
+ .timeline-item {
3575
+ position: relative;
3576
+ margin: 1rem 0;
3577
+ padding-left: 1rem;
3578
+ }
3579
+
3580
+ .timeline-item::before {
3581
+ content: '';
3582
+ position: absolute;
3583
+ left: -1.15rem;
3584
+ top: 0.5rem;
3585
+ width: 10px;
3586
+ height: 10px;
3587
+ border-radius: 50%;
3588
+ background: var(--accent);
3589
+ border: 2px solid var(--bg);
3590
+ }
3591
+
3592
+ .timeline-item.decision::before {
3593
+ background: var(--warning);
3594
+ }
3595
+
3596
+ .timeline-item.chapter::before {
3597
+ background: var(--success);
3598
+ }
3599
+
3600
+ .timeline-time {
3601
+ font-size: 0.75rem;
3602
+ color: var(--text-muted);
3603
+ }
3604
+
3605
+ .timeline-content {
3606
+ margin-top: 0.25rem;
3607
+ }
3608
+
3609
+ .chapter {
3610
+ background: var(--bg-secondary);
3611
+ border-radius: 8px;
3612
+ padding: 1rem;
3613
+ margin: 1rem 0;
3614
+ }
3615
+
3616
+ .chapter-title {
3617
+ font-weight: 600;
3618
+ display: flex;
3619
+ align-items: center;
3620
+ gap: 0.5rem;
3621
+ }
3622
+
3623
+ .chapter-agent {
3624
+ font-size: 0.85rem;
3625
+ color: var(--text-muted);
3626
+ }
3627
+
3628
+ .retrospective {
3629
+ background: linear-gradient(135deg, var(--accent-light), var(--bg-secondary));
3630
+ border-radius: 8px;
3631
+ padding: 1.5rem;
3632
+ margin: 1.5rem 0;
3633
+ }
3634
+
3635
+ .retrospective h3 {
3636
+ margin-bottom: 1rem;
3637
+ }
3638
+
3639
+ .confidence {
3640
+ display: flex;
3641
+ align-items: center;
3642
+ gap: 0.5rem;
3643
+ margin: 0.5rem 0;
3644
+ }
3645
+
3646
+ .confidence-bar {
3647
+ flex: 1;
3648
+ height: 8px;
3649
+ background: var(--border);
3650
+ border-radius: 4px;
3651
+ overflow: hidden;
3652
+ max-width: 200px;
3653
+ }
3654
+
3655
+ .confidence-fill {
3656
+ height: 100%;
3657
+ background: var(--accent);
3658
+ transition: width 0.3s;
3659
+ }
3660
+
3661
+ .list {
3662
+ list-style: none;
3663
+ }
3664
+
3665
+ .list li {
3666
+ padding: 0.25rem 0;
3667
+ padding-left: 1rem;
3668
+ position: relative;
3669
+ }
3670
+
3671
+ .list li::before {
3672
+ content: '\u2022';
3673
+ position: absolute;
3674
+ left: 0;
3675
+ color: var(--accent);
3676
+ }
3677
+
3678
+ .files-changed {
3679
+ font-family: monospace;
3680
+ font-size: 0.85rem;
3681
+ background: var(--bg-secondary);
3682
+ padding: 0.75rem;
3683
+ border-radius: 4px;
3684
+ margin: 0.5rem 0;
3685
+ }
3686
+
3687
+ .empty {
3688
+ color: var(--text-muted);
3689
+ font-style: italic;
3690
+ }
3691
+
3692
+ /* Index page styles */
3693
+ .trajectory-list {
3694
+ list-style: none;
3695
+ }
3696
+
3697
+ .trajectory-card {
3698
+ display: block;
3699
+ padding: 1rem;
3700
+ margin: 0.5rem 0;
3701
+ background: var(--bg-secondary);
3702
+ border-radius: 8px;
3703
+ text-decoration: none;
3704
+ color: var(--text);
3705
+ border: 1px solid var(--border);
3706
+ transition: border-color 0.2s, box-shadow 0.2s;
3707
+ }
3708
+
3709
+ .trajectory-card:hover {
3710
+ border-color: var(--accent);
3711
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
3712
+ }
3713
+
3714
+ .trajectory-card-title {
3715
+ font-weight: 600;
3716
+ margin-bottom: 0.25rem;
3717
+ }
3718
+
3719
+ .trajectory-card-meta {
3720
+ font-size: 0.85rem;
3721
+ color: var(--text-muted);
3722
+ display: flex;
3723
+ gap: 1rem;
3724
+ }
3725
+
3726
+ .group-header {
3727
+ font-size: 0.85rem;
3728
+ text-transform: uppercase;
3729
+ color: var(--text-muted);
3730
+ letter-spacing: 0.05em;
3731
+ margin-top: 1.5rem;
3732
+ margin-bottom: 0.5rem;
3733
+ }
3734
+ `;
3735
+
3736
+ // src/web/generator.ts
3737
+ function escapeHtml(text) {
3738
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
3739
+ }
3740
+ function formatDate4(isoDate) {
3741
+ const date = new Date(isoDate);
3742
+ return date.toLocaleDateString("en-US", {
3743
+ month: "short",
3744
+ day: "numeric",
3745
+ year: "numeric",
3746
+ hour: "2-digit",
3747
+ minute: "2-digit"
3748
+ });
3749
+ }
3750
+ function formatDuration2(startDate, endDate) {
3751
+ const start = new Date(startDate).getTime();
3752
+ const end = endDate ? new Date(endDate).getTime() : Date.now();
3753
+ const ms = end - start;
3754
+ const seconds = Math.floor(ms / 1e3);
3755
+ const minutes = Math.floor(seconds / 60);
3756
+ const hours = Math.floor(minutes / 60);
3757
+ const days = Math.floor(hours / 24);
3758
+ if (days > 0) return `${days}d ${hours % 24}h`;
3759
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
3760
+ if (minutes > 0) return `${minutes}m`;
3761
+ return `${seconds}s`;
3762
+ }
3763
+ function getStatusClass(status) {
3764
+ switch (status) {
3765
+ case "active":
3766
+ return "status-active";
3767
+ case "completed":
3768
+ return "status-completed";
3769
+ case "abandoned":
3770
+ return "status-abandoned";
3771
+ default:
3772
+ return "";
3773
+ }
3774
+ }
3775
+ function renderDecision(decision) {
3776
+ const alternatives = decision.alternatives?.length ? `<div class="alternatives">
3777
+ <span class="alternatives-label">Considered:</span>
3778
+ ${decision.alternatives.map((a) => escapeHtml(typeof a === "string" ? a : a.option)).join(", ")}
3779
+ </div>` : "";
3780
+ return `
3781
+ <div class="decision">
3782
+ <div class="decision-title">${escapeHtml(decision.question)}: ${escapeHtml(decision.chosen)}</div>
3783
+ <div class="decision-reasoning">${escapeHtml(decision.reasoning)}</div>
3784
+ ${alternatives}
3785
+ </div>
3786
+ `;
3787
+ }
3788
+ function renderEvent(event) {
3789
+ const time = formatDate4(new Date(event.ts).toISOString());
3790
+ let content = "";
3791
+ let typeClass = "";
3792
+ const rawData = event.raw;
3793
+ switch (event.type) {
3794
+ case "decision":
3795
+ typeClass = "decision";
3796
+ content = `
3797
+ <strong>Decision:</strong> ${escapeHtml(event.content)}
3798
+ ${rawData?.reasoning ? `<div class="decision-reasoning">${escapeHtml(String(rawData.reasoning))}</div>` : ""}
3799
+ `;
3800
+ break;
3801
+ case "thinking":
3802
+ content = `<strong>Thinking:</strong> ${escapeHtml(event.content)}`;
3803
+ break;
3804
+ case "prompt":
3805
+ content = `<strong>Prompt:</strong> ${escapeHtml(event.content)}`;
3806
+ break;
3807
+ case "tool_call":
3808
+ content = `<strong>Tool:</strong> <code>${escapeHtml(event.content)}</code>`;
3809
+ break;
3810
+ case "tool_result":
3811
+ content = `<strong>Result:</strong> ${escapeHtml(event.content)}`;
3812
+ break;
3813
+ case "message_sent":
3814
+ content = `<strong>Sent:</strong> ${escapeHtml(event.content)}`;
3815
+ break;
3816
+ case "message_received":
3817
+ content = `<strong>Received:</strong> ${escapeHtml(event.content)}`;
3818
+ break;
3819
+ case "error":
3820
+ content = `<strong style="color: var(--error)">Error:</strong> ${escapeHtml(event.content)}`;
3821
+ break;
3822
+ case "note":
3823
+ content = escapeHtml(event.content);
3824
+ break;
3825
+ default:
3826
+ content = escapeHtml(event.content);
3827
+ }
3828
+ return `
3829
+ <div class="timeline-item ${typeClass}">
3830
+ <div class="timeline-time">${time}</div>
3831
+ <div class="timeline-content">${content}</div>
3832
+ </div>
3833
+ `;
3834
+ }
3835
+ function renderChapter2(chapter, index) {
3836
+ const events = chapter.events.map(renderEvent).join("");
3837
+ return `
3838
+ <div class="chapter">
3839
+ <div class="chapter-title">
3840
+ Chapter ${index + 1}: ${escapeHtml(chapter.title)}
3841
+ </div>
3842
+ <div class="chapter-agent">Agent: ${escapeHtml(chapter.agentName)}</div>
3843
+ ${chapter.events.length > 0 ? `
3844
+ <h3 class="collapsible" onclick="this.classList.toggle('open')">Events (${chapter.events.length})</h3>
3845
+ <div class="collapsible-content">
3846
+ <div class="timeline">${events}</div>
3847
+ </div>
3848
+ ` : ""}
3849
+ </div>
3850
+ `;
3851
+ }
3852
+ function renderRetrospective2(trajectory) {
3853
+ if (!trajectory.retrospective) {
3854
+ return "";
3855
+ }
3856
+ const retro = trajectory.retrospective;
3857
+ const confidencePercent = Math.round(retro.confidence * 100);
3858
+ const approach = retro.approach ? `<div><strong>Approach:</strong><p>${escapeHtml(retro.approach)}</p></div>` : "";
3859
+ const learnings = retro.learnings?.length ? `<div><strong>Learnings:</strong><ul class="list">${retro.learnings.map((l) => `<li>${escapeHtml(l)}</li>`).join("")}</ul></div>` : "";
3860
+ const challenges = retro.challenges?.length ? `<div><strong>Challenges:</strong><ul class="list">${retro.challenges.map((c) => `<li>${escapeHtml(c)}</li>`).join("")}</ul></div>` : "";
3861
+ const suggestions = retro.suggestions?.length ? `<div><strong>Suggestions:</strong><ul class="list">${retro.suggestions.map((s) => `<li>${escapeHtml(s)}</li>`).join("")}</ul></div>` : "";
3862
+ return `
3863
+ <div class="retrospective">
3864
+ <h3>\u{1F4DD} Retrospective</h3>
3865
+ <p>${escapeHtml(retro.summary)}</p>
3866
+
3867
+ <div class="confidence">
3868
+ <span>Confidence:</span>
3869
+ <div class="confidence-bar">
3870
+ <div class="confidence-fill" style="width: ${confidencePercent}%"></div>
3871
+ </div>
3872
+ <span>${confidencePercent}%</span>
3873
+ </div>
3874
+
3875
+ ${approach}
3876
+ ${learnings}
3877
+ ${challenges}
3878
+ ${suggestions}
3879
+ </div>
3880
+ `;
3881
+ }
3882
+ function generateTrajectoryHtml(trajectory) {
3883
+ const statusClass = getStatusClass(trajectory.status);
3884
+ const duration = formatDuration2(trajectory.startedAt, trajectory.completedAt);
3885
+ const decisions = trajectory.chapters.flatMap(
3886
+ (ch) => ch.events.filter((e) => e.type === "decision" && e.raw).map((e) => e.raw).filter(
3887
+ (d) => d !== void 0 && typeof d.question === "string"
3888
+ )
3889
+ );
3890
+ const decisionsHtml = decisions.length ? `
3891
+ <h2 class="collapsible open" onclick="this.classList.toggle('open')">
3892
+ Key Decisions (${decisions.length})
3893
+ </h2>
3894
+ <div class="collapsible-content">
3895
+ ${decisions.map(renderDecision).join("")}
3896
+ </div>
3897
+ ` : "";
3898
+ const chaptersHtml = trajectory.chapters.length ? `
3899
+ <h2 class="collapsible open" onclick="this.classList.toggle('open')">
3900
+ Chapters (${trajectory.chapters.length})
3901
+ </h2>
3902
+ <div class="collapsible-content">
3903
+ ${trajectory.chapters.map(renderChapter2).join("")}
3904
+ </div>
3905
+ ` : "";
3906
+ const filesHtml = trajectory.filesChanged.length ? `
3907
+ <h2>Files Changed (${trajectory.filesChanged.length})</h2>
3908
+ <div class="files-changed">
3909
+ ${trajectory.filesChanged.map((f) => escapeHtml(f)).join("<br>")}
3910
+ </div>
3911
+ ` : "";
3912
+ const commitsHtml = trajectory.commits.length ? `
3913
+ <h2>Commits (${trajectory.commits.length})</h2>
3914
+ <div class="files-changed">
3915
+ ${trajectory.commits.map((c) => `<code>${escapeHtml(c)}</code>`).join("<br>")}
3916
+ </div>
3917
+ ` : "";
3918
+ return `<!DOCTYPE html>
3919
+ <html lang="en">
3920
+ <head>
3921
+ <meta charset="UTF-8">
3922
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3923
+ <title>${escapeHtml(trajectory.task.title)} - Trajectory</title>
3924
+ <style>${styles}</style>
3925
+ </head>
3926
+ <body>
3927
+ <div class="header">
3928
+ <h1>\u{1F6E4}\uFE0F ${escapeHtml(trajectory.task.title)}</h1>
3929
+ <div class="meta">
3930
+ <div class="meta-item">
3931
+ <span class="meta-label">ID</span>
3932
+ <span class="meta-value"><code>${trajectory.id}</code></span>
3933
+ </div>
3934
+ <div class="meta-item">
3935
+ <span class="meta-label">Status</span>
3936
+ <span class="meta-value"><span class="status ${statusClass}">${trajectory.status}</span></span>
3937
+ </div>
3938
+ <div class="meta-item">
3939
+ <span class="meta-label">Duration</span>
3940
+ <span class="meta-value">${duration}</span>
3941
+ </div>
3942
+ <div class="meta-item">
3943
+ <span class="meta-label">Started</span>
3944
+ <span class="meta-value">${formatDate4(trajectory.startedAt)}</span>
3945
+ </div>
3946
+ ${trajectory.task.source ? `
3947
+ <div class="meta-item">
3948
+ <span class="meta-label">Source</span>
3949
+ <span class="meta-value">${escapeHtml(trajectory.task.source.system)}:${escapeHtml(trajectory.task.source.id)}</span>
3950
+ </div>
3951
+ ` : ""}
3952
+ <div class="meta-item">
3953
+ <span class="meta-label">Agents</span>
3954
+ <span class="meta-value">${trajectory.agents.map((a) => escapeHtml(a.name)).join(", ") || "\u2014"}</span>
3955
+ </div>
3956
+ </div>
3957
+ </div>
3958
+
3959
+ ${renderRetrospective2(trajectory)}
3960
+ ${decisionsHtml}
3961
+ ${chaptersHtml}
3962
+ ${filesHtml}
3963
+ ${commitsHtml}
3964
+
3965
+ <script>
3966
+ // Initialize all collapsible sections
3967
+ document.querySelectorAll('.collapsible.open').forEach(el => {
3968
+ el.nextElementSibling?.style && (el.nextElementSibling.style.display = 'block');
3969
+ });
3970
+ </script>
3971
+ </body>
3972
+ </html>`;
3973
+ }
3974
+
3975
+ // src/cli/commands/export.ts
3976
+ function registerExportCommand(program2) {
3977
+ program2.command("export [id]").description("Export a trajectory").option(
3978
+ "-f, --format <format>",
3979
+ "Export format (md, json, timeline, html)",
3980
+ "md"
3981
+ ).option("-o, --output <path>", "Output file path").option("--open", "Open in browser (html format only)").action(async (id, options) => {
3982
+ const storage = new FileStorage();
3983
+ await storage.initialize();
3984
+ let trajectory;
3985
+ if (id) {
3986
+ trajectory = await storage.get(id);
3987
+ if (!trajectory) {
3988
+ console.error(`Error: Trajectory not found: ${id}`);
3989
+ throw new Error("Trajectory not found");
3990
+ }
3991
+ } else {
3992
+ trajectory = await storage.getActive();
3993
+ if (!trajectory) {
3994
+ console.error("Error: No active trajectory and no ID provided");
3995
+ console.error(
3996
+ "Usage: trail export <id> or trail export (with active trajectory)"
3997
+ );
3998
+ throw new Error("No trajectory specified");
3999
+ }
4000
+ }
4001
+ let output;
4002
+ switch (options.format) {
4003
+ case "json":
4004
+ output = exportToJSON(trajectory);
4005
+ break;
4006
+ case "timeline":
4007
+ output = exportToTimeline(trajectory);
4008
+ break;
4009
+ case "html":
4010
+ output = generateTrajectoryHtml(trajectory);
4011
+ break;
4012
+ default:
4013
+ output = exportToMarkdown(trajectory);
4014
+ break;
4015
+ }
4016
+ if (options.output) {
4017
+ await writeFile4(options.output, output, "utf-8");
4018
+ console.log(`\u2713 Exported to ${options.output}`);
4019
+ if (options.open && options.format === "html") {
4020
+ openInBrowser(options.output);
4021
+ }
4022
+ } else if (options.open && options.format === "html") {
4023
+ const outputDir = join7(process.cwd(), ".trajectories", "html");
4024
+ await mkdir4(outputDir, { recursive: true });
4025
+ const filePath = join7(outputDir, `${trajectory.id}.html`);
4026
+ await writeFile4(filePath, output, "utf-8");
4027
+ console.log(`\u2713 Generated: ${filePath}`);
4028
+ openInBrowser(filePath);
4029
+ } else {
4030
+ console.log(output);
4031
+ }
4032
+ });
4033
+ }
4034
+ function openInBrowser(path2) {
4035
+ const platform = process.platform;
4036
+ let command;
4037
+ if (platform === "darwin") {
4038
+ command = `open "${path2}"`;
4039
+ } else if (platform === "win32") {
4040
+ command = `start "" "${path2}"`;
4041
+ } else {
4042
+ command = `xdg-open "${path2}"`;
4043
+ }
4044
+ exec(command, (error) => {
4045
+ if (error) {
4046
+ console.log(`Open manually: file://${path2}`);
4047
+ }
4048
+ });
4049
+ }
4050
+
4051
+ // src/cli/commands/list.ts
4052
+ import { existsSync as existsSync6 } from "fs";
4053
+ function registerListCommand(program2) {
4054
+ program2.command("list").description("List and search trajectories").option(
4055
+ "-s, --status <status>",
4056
+ "Filter by status (active, completed, abandoned)"
4057
+ ).option("-l, --limit <number>", "Limit results", Number.parseInt).option("--search <query>", "Search trajectories by title or content").action(async (options) => {
4058
+ const searchPaths = getSearchPaths();
4059
+ let allTrajectories = [];
4060
+ const seenIds = /* @__PURE__ */ new Set();
4061
+ for (const searchPath of searchPaths) {
4062
+ if (!existsSync6(searchPath)) {
4063
+ continue;
4064
+ }
4065
+ const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
4066
+ process.env.TRAJECTORIES_DATA_DIR = searchPath;
4067
+ try {
4068
+ const storage = new FileStorage();
4069
+ await storage.initialize();
4070
+ const trajectories = await storage.list({
4071
+ status: options.status,
4072
+ limit: options.search ? void 0 : void 0
4073
+ // Don't limit per-path
4074
+ });
4075
+ for (const traj of trajectories) {
4076
+ if (!seenIds.has(traj.id)) {
4077
+ seenIds.add(traj.id);
4078
+ allTrajectories.push(traj);
4079
+ }
4080
+ }
4081
+ } finally {
4082
+ if (originalDataDir !== void 0) {
4083
+ process.env.TRAJECTORIES_DATA_DIR = originalDataDir;
4084
+ } else {
4085
+ process.env.TRAJECTORIES_DATA_DIR = void 0;
4086
+ }
4087
+ }
4088
+ }
4089
+ allTrajectories.sort(
4090
+ (a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
4091
+ );
4092
+ if (options.search) {
4093
+ const query = options.search.toLowerCase();
4094
+ allTrajectories = allTrajectories.filter((traj) => {
4095
+ if (traj.title.toLowerCase().includes(query)) return true;
4096
+ if (traj.id.toLowerCase().includes(query)) return true;
4097
+ return false;
4098
+ });
4099
+ }
4100
+ if (options.limit) {
4101
+ allTrajectories = allTrajectories.slice(0, options.limit);
4102
+ }
4103
+ if (allTrajectories.length === 0) {
4104
+ if (options.search) {
4105
+ console.log(`No trajectories found matching "${options.search}"`);
4106
+ } else {
4107
+ console.log("No trajectories found");
4108
+ }
4109
+ return;
4110
+ }
4111
+ const searchNote = options.search ? ` matching "${options.search}"` : "";
4112
+ console.log(
4113
+ `Found ${allTrajectories.length} trajectories${searchNote}:
4114
+ `
4115
+ );
4116
+ for (const traj of allTrajectories) {
4117
+ const statusIcon = getStatusIcon(traj.status);
4118
+ const confidence = traj.confidence ? ` (${Math.round(traj.confidence * 100)}%)` : "";
4119
+ console.log(`${statusIcon} ${traj.id}`);
4120
+ console.log(` ${traj.title}${confidence}`);
4121
+ console.log(` Started: ${formatDate5(traj.startedAt)}`);
4122
+ if (traj.completedAt) {
4123
+ console.log(` Completed: ${formatDate5(traj.completedAt)}`);
4124
+ }
4125
+ console.log("");
4126
+ }
4127
+ });
4128
+ }
4129
+ function getStatusIcon(status) {
4130
+ switch (status) {
4131
+ case "active":
4132
+ return "\u{1F504}";
4133
+ case "completed":
4134
+ return "\u2705";
4135
+ case "abandoned":
4136
+ return "\u274C";
4137
+ default:
4138
+ return "\u2022";
4139
+ }
4140
+ }
4141
+ function formatDate5(isoString) {
4142
+ return new Date(isoString).toLocaleDateString("en-US", {
4143
+ month: "short",
4144
+ day: "numeric",
4145
+ year: "numeric"
4146
+ });
4147
+ }
4148
+
4149
+ // src/cli/commands/reflect.ts
4150
+ function registerReflectCommand(program2) {
4151
+ program2.command("reflect <synthesis>").description("Record a reflection (periodic synthesis of progress)").option(
4152
+ "-f, --focal-points <items>",
4153
+ "Comma-separated focal points that prompted this reflection"
4154
+ ).option(
4155
+ "-c, --confidence <number>",
4156
+ "Confidence in current trajectory (0-1)",
4157
+ Number.parseFloat
4158
+ ).option(
4159
+ "-a, --adjustments <text>",
4160
+ "Course corrections triggered by this reflection"
4161
+ ).action(async (synthesis, options) => {
4162
+ const storage = new FileStorage();
4163
+ await storage.initialize();
4164
+ const active = await storage.getActive();
4165
+ if (!active) {
4166
+ console.error("Error: No active trajectory");
4167
+ console.error('Start one with: trail start "Task description"');
4168
+ throw new Error("No active trajectory");
4169
+ }
4170
+ const tags = [];
4171
+ if (options.focalPoints) {
4172
+ for (const fp of options.focalPoints.split(",")) {
4173
+ tags.push(`focal:${fp.trim()}`);
4174
+ }
4175
+ }
4176
+ if (options.confidence !== void 0) {
4177
+ tags.push(`confidence:${options.confidence}`);
4178
+ }
4179
+ const raw = {};
4180
+ if (options.focalPoints) {
4181
+ raw.focalPoints = options.focalPoints.split(",").map((s) => s.trim());
4182
+ }
4183
+ if (options.adjustments) {
4184
+ raw.adjustments = options.adjustments;
4185
+ }
4186
+ if (options.confidence !== void 0) {
4187
+ raw.confidence = options.confidence;
4188
+ }
4189
+ const updated = addEvent(active, {
4190
+ type: "reflection",
4191
+ content: synthesis,
4192
+ significance: "high",
4193
+ tags: tags.length > 0 ? tags : void 0,
4194
+ raw: Object.keys(raw).length > 0 ? raw : void 0
4195
+ });
4196
+ await storage.save(updated);
4197
+ console.log(`\u2713 Reflection recorded: ${synthesis}`);
4198
+ if (options.focalPoints) {
4199
+ console.log(` Focal points: ${options.focalPoints}`);
4200
+ }
4201
+ if (options.confidence !== void 0) {
4202
+ console.log(` Confidence: ${options.confidence}`);
4203
+ }
4204
+ if (options.adjustments) {
4205
+ console.log(` Adjustments: ${options.adjustments}`);
4206
+ }
4207
+ });
4208
+ }
4209
+
4210
+ // src/cli/commands/show.ts
4211
+ import { existsSync as existsSync7 } from "fs";
4212
+ import { readFile as readFile2 } from "fs/promises";
4213
+ import { join as join8 } from "path";
4214
+ async function findTrajectory(id) {
4215
+ const searchPaths = getSearchPaths();
4216
+ for (const searchPath of searchPaths) {
4217
+ if (!existsSync7(searchPath)) {
4218
+ continue;
4219
+ }
4220
+ const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
4221
+ process.env.TRAJECTORIES_DATA_DIR = searchPath;
4222
+ try {
4223
+ const storage = new FileStorage();
4224
+ await storage.initialize();
4225
+ const trajectory = await storage.get(id);
4226
+ if (trajectory) {
4227
+ return trajectory;
4228
+ }
4229
+ } finally {
4230
+ if (originalDataDir !== void 0) {
4231
+ process.env.TRAJECTORIES_DATA_DIR = originalDataDir;
4232
+ } else {
4233
+ process.env.TRAJECTORIES_DATA_DIR = void 0;
4234
+ }
4235
+ }
4236
+ }
4237
+ return null;
4238
+ }
4239
+ async function findTraceFile(id) {
4240
+ const searchPaths = getSearchPaths();
4241
+ for (const searchPath of searchPaths) {
4242
+ if (!existsSync7(searchPath)) {
4243
+ continue;
4244
+ }
4245
+ const completedDir = join8(searchPath, "completed");
4246
+ if (!existsSync7(completedDir)) {
4247
+ continue;
4248
+ }
4249
+ try {
4250
+ const { readdirSync } = await import("fs");
4251
+ const months = readdirSync(completedDir);
4252
+ for (const month of months) {
4253
+ const tracePath = join8(completedDir, month, `${id}.trace.json`);
4254
+ if (existsSync7(tracePath)) {
4255
+ let record;
4256
+ let migrated;
4257
+ try {
4258
+ const content = await readFile2(tracePath, "utf-8");
4259
+ ({ record, migrated } = migrateTraceRecord(JSON.parse(content)));
4260
+ } catch {
4261
+ continue;
4262
+ }
4263
+ if (migrated) {
4264
+ try {
4265
+ const { writeFile: writeFile5 } = await import("fs/promises");
4266
+ await writeFile5(
4267
+ tracePath,
4268
+ JSON.stringify(record, null, 2),
4269
+ "utf-8"
4270
+ );
4271
+ } catch {
4272
+ }
4273
+ }
4274
+ return record;
4275
+ }
4276
+ }
4277
+ } catch {
4278
+ }
4279
+ }
4280
+ return null;
4281
+ }
4282
+ function registerShowCommand(program2) {
4283
+ program2.command("show <id>").description("Show trajectory details").option("-d, --decisions", "Show decisions only").option("-t, --trace", "Show trace information").action(async (id, options) => {
4284
+ const trajectory = await findTrajectory(id);
4285
+ if (!trajectory) {
4286
+ console.error(`Error: Trajectory not found: ${id}`);
4287
+ throw new Error("Trajectory not found");
4288
+ }
4289
+ if (options.trace) {
4290
+ console.log(`Trace for ${trajectory.task.title}:
4291
+ `);
4292
+ if (trajectory._trace) {
4293
+ console.log("Trace Reference:");
4294
+ console.log(` Start Ref: ${trajectory._trace.startRef}`);
4295
+ if (trajectory._trace.endRef) {
4296
+ console.log(` End Ref: ${trajectory._trace.endRef}`);
4297
+ }
4298
+ if (trajectory._trace.traceId) {
4299
+ console.log(` Trace ID: ${trajectory._trace.traceId}`);
4300
+ }
4301
+ console.log("");
4302
+ }
4303
+ const trace = await findTraceFile(id);
4304
+ if (trace) {
4305
+ console.log("Trace Details:");
4306
+ console.log(` ID: ${trace.id}`);
4307
+ console.log(` Timestamp: ${trace.timestamp}`);
4308
+ console.log(` Files: ${trace.files.length}`);
4309
+ console.log("");
4310
+ if (trace.files.length > 0) {
4311
+ console.log("Modified Files:");
4312
+ for (const file of trace.files) {
4313
+ const rangeCount = file.conversations.reduce(
4314
+ (sum, conv) => sum + conv.ranges.length,
4315
+ 0
4316
+ );
4317
+ const model = file.conversations[0]?.contributor.model_id ?? "unknown";
4318
+ console.log(` \u2022 ${file.path}`);
4319
+ console.log(` Ranges: ${rangeCount}, Model: ${model}`);
4320
+ }
4321
+ }
4322
+ } else if (!trajectory._trace) {
4323
+ console.log("No trace information available");
4324
+ console.log(
4325
+ "Trace is captured when starting a trajectory in a git repo"
4326
+ );
4327
+ }
4328
+ return;
4329
+ }
4330
+ if (options.decisions) {
4331
+ const decisions = extractDecisions2(trajectory);
4332
+ if (decisions.length === 0) {
4333
+ console.log("No decisions recorded");
4334
+ return;
4335
+ }
4336
+ console.log(`Decisions for ${trajectory.task.title}:
4337
+ `);
4338
+ for (const decision of decisions) {
4339
+ console.log(`\u2022 ${decision.question}`);
4340
+ console.log(` Chose: ${decision.chosen}`);
4341
+ console.log(` Reasoning: ${decision.reasoning}`);
4342
+ if (decision.alternatives.length > 0) {
4343
+ const altStrings = decision.alternatives.map(
4344
+ (a) => typeof a === "string" ? a : a.option
4345
+ );
4346
+ console.log(` Alternatives: ${altStrings.join(", ")}`);
4347
+ }
4348
+ console.log("");
4349
+ }
4350
+ return;
4351
+ }
4352
+ console.log(`Trajectory: ${trajectory.id}`);
4353
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
4354
+ console.log(`Title: ${trajectory.task.title}`);
4355
+ console.log(`Status: ${trajectory.status}`);
4356
+ console.log(`Started: ${trajectory.startedAt}`);
4357
+ if (trajectory.completedAt) {
4358
+ console.log(`Ended: ${trajectory.completedAt}`);
4359
+ }
4360
+ if (trajectory.task.source) {
4361
+ console.log(
4362
+ `Source: ${trajectory.task.source.system}:${trajectory.task.source.id}`
4363
+ );
4364
+ }
4365
+ console.log(`
4366
+ Chapters: ${trajectory.chapters.length}`);
4367
+ for (const chapter of trajectory.chapters) {
4368
+ console.log(` \u2022 ${chapter.title} (${chapter.agentName})`);
4369
+ console.log(` Events: ${chapter.events.length}`);
4370
+ }
4371
+ if (trajectory.retrospective) {
4372
+ console.log("\nRetrospective:");
4373
+ console.log(` Summary: ${trajectory.retrospective.summary}`);
4374
+ console.log(
4375
+ ` Confidence: ${Math.round(trajectory.retrospective.confidence * 100)}%`
4376
+ );
4377
+ }
4378
+ });
4379
+ }
4380
+ function extractDecisions2(trajectory) {
4381
+ const decisions = [];
4382
+ if (trajectory.retrospective?.decisions) {
4383
+ decisions.push(...trajectory.retrospective.decisions);
4384
+ }
4385
+ for (const chapter of trajectory.chapters) {
4386
+ for (const event of chapter.events) {
4387
+ if (event.type === "decision" && event.raw) {
4388
+ const raw = event.raw;
4389
+ if (raw.question && raw.chosen && raw.reasoning) {
4390
+ if (!decisions.some((d) => d.question === raw.question)) {
4391
+ decisions.push(raw);
4392
+ }
4393
+ }
4394
+ }
4395
+ }
4396
+ }
4397
+ return decisions;
4398
+ }
4399
+
4400
+ // src/cli/commands/start.ts
4401
+ function registerStartCommand(program2) {
4402
+ program2.command("start <title>").description("Start a new trajectory").option("-t, --task <id>", "External task ID").option(
4403
+ "-s, --source <system>",
4404
+ "Task system (github, linear, jira, beads)"
4405
+ ).option("--url <url>", "URL to external task").option("-a, --agent <name>", "Agent name (or set TRAJECTORIES_AGENT)").option("-p, --project <id>", "Project ID (or set TRAJECTORIES_PROJECT)").option(
4406
+ "-w, --workflow <id>",
4407
+ "Workflow run id (or set TRAJECTORIES_WORKFLOW_ID). Stamped onto the trajectory so `trail compact --workflow <id>` can collate a run."
4408
+ ).option("-q, --quiet", "Only output trajectory ID (for scripting)").action(async (title, options) => {
4409
+ const storage = new FileStorage();
4410
+ await storage.initialize();
4411
+ const active = await storage.getActive();
4412
+ if (active) {
4413
+ if (!options.quiet) {
4414
+ console.error(`Error: Trajectory already active: ${active.id}`);
4415
+ console.error(
4416
+ "Complete or abandon it first with: trail complete or trail abandon"
4417
+ );
4418
+ }
4419
+ throw new Error("Trajectory already active");
4420
+ }
4421
+ let source;
4422
+ if (options.task) {
4423
+ source = {
4424
+ system: options.source || "plain",
4425
+ id: options.task,
4426
+ url: options.url
4427
+ };
4428
+ }
4429
+ const agentName = options.agent ?? process.env.TRAJECTORIES_AGENT ?? void 0;
4430
+ const projectId = options.project ?? process.env.TRAJECTORIES_PROJECT ?? void 0;
4431
+ const workflowId = typeof options.workflow === "string" && options.workflow.trim() || typeof process.env.TRAJECTORIES_WORKFLOW_ID === "string" && process.env.TRAJECTORIES_WORKFLOW_ID.trim() || void 0;
4432
+ const startRef = captureGitState();
4433
+ let trajectory = createTrajectory({
4434
+ title,
4435
+ source,
4436
+ projectId
4437
+ });
4438
+ if (workflowId) {
4439
+ trajectory = { ...trajectory, workflowId };
4440
+ }
4441
+ if (startRef) {
4442
+ trajectory = {
4443
+ ...trajectory,
4444
+ _trace: createTraceRef(startRef)
4445
+ };
4446
+ }
4447
+ if (agentName) {
4448
+ trajectory = addChapter(trajectory, {
4449
+ title: "Initial work",
4450
+ agentName
4451
+ });
4452
+ }
4453
+ await storage.save(trajectory);
4454
+ if (options.quiet) {
4455
+ console.log(trajectory.id);
4456
+ } else {
4457
+ console.log(`\u2713 Trajectory started: ${trajectory.id}`);
4458
+ console.log(` Title: ${title}`);
4459
+ if (agentName) {
4460
+ console.log(` Agent: ${agentName}`);
4461
+ }
4462
+ if (projectId) {
4463
+ console.log(` Project: ${projectId}`);
4464
+ }
4465
+ if (source) {
4466
+ console.log(` Linked to: ${source.id} (${source.system})`);
4467
+ }
4468
+ }
4469
+ });
4470
+ }
4471
+
4472
+ // src/cli/commands/status.ts
4473
+ function registerStatusCommand(program2) {
4474
+ program2.command("status").description("Show active trajectory status").action(async () => {
4475
+ const storage = new FileStorage();
4476
+ await storage.initialize();
4477
+ const active = await storage.getActive();
4478
+ if (!active) {
4479
+ console.log("No active trajectory");
4480
+ console.log('Start one with: trail start "Task description"');
4481
+ return;
4482
+ }
4483
+ const duration = formatDuration3(
4484
+ (/* @__PURE__ */ new Date()).getTime() - new Date(active.startedAt).getTime()
4485
+ );
4486
+ const eventCount = active.chapters.reduce(
4487
+ (sum, ch) => sum + ch.events.length,
4488
+ 0
4489
+ );
4490
+ const decisionCount = active.chapters.reduce(
4491
+ (sum, ch) => sum + ch.events.filter((e) => e.type === "decision").length,
4492
+ 0
4493
+ );
4494
+ console.log(`Active Trajectory: ${active.id}`);
4495
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
4496
+ console.log(`Task: ${active.task.title}`);
4497
+ if (active.task.source) {
4498
+ console.log(
4499
+ `Source: ${active.task.source.system}:${active.task.source.id}`
4500
+ );
4501
+ }
4502
+ console.log(`Status: ${active.status}`);
4503
+ console.log(`Started: ${duration} ago`);
4504
+ console.log(`Chapters: ${active.chapters.length}`);
4505
+ console.log(`Events: ${eventCount}`);
4506
+ console.log(`Decisions: ${decisionCount}`);
4507
+ if (active._trace?.startRef) {
4508
+ const commits = getCommitsBetween(active._trace.startRef);
4509
+ const filesChanged = getFilesChangedBetween(active._trace.startRef);
4510
+ if (commits.length > 0) {
4511
+ console.log(`Commits: ${commits.length}`);
4512
+ }
4513
+ if (filesChanged.length > 0) {
4514
+ console.log(`Files: ${filesChanged.length}`);
4515
+ }
4516
+ }
4517
+ if (active.chapters.length > 0) {
4518
+ const currentChapter = active.chapters[active.chapters.length - 1];
4519
+ console.log(`
4520
+ Current Chapter: ${currentChapter.title}`);
4521
+ console.log(` Agent: ${currentChapter.agentName}`);
4522
+ }
4523
+ });
4524
+ }
4525
+ function formatDuration3(ms) {
4526
+ const seconds = Math.floor(ms / 1e3);
4527
+ const minutes = Math.floor(seconds / 60);
4528
+ const hours = Math.floor(minutes / 60);
4529
+ const days = Math.floor(hours / 24);
4530
+ if (days > 0) return `${days}d ${hours % 24}h`;
4531
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
4532
+ if (minutes > 0) return `${minutes}m`;
4533
+ return `${seconds}s`;
4534
+ }
4535
+
4536
+ // src/cli/commands/index.ts
4537
+ function registerCommands(program2) {
4538
+ registerStartCommand(program2);
4539
+ registerStatusCommand(program2);
4540
+ registerCompleteCommand(program2);
4541
+ registerAbandonCommand(program2);
4542
+ registerDecisionCommand(program2);
4543
+ registerReflectCommand(program2);
4544
+ registerListCommand(program2);
4545
+ registerShowCommand(program2);
4546
+ registerExportCommand(program2);
4547
+ registerEnableCommand(program2);
4548
+ registerCompactCommand(program2);
4549
+ }
4550
+
4551
+ // src/cli/version.ts
4552
+ import fs from "fs";
4553
+ import path from "path";
4554
+ import { fileURLToPath } from "url";
4555
+ function findPackageJson(startDir) {
4556
+ let dir = startDir;
4557
+ while (dir !== path.dirname(dir)) {
4558
+ const candidate = path.join(dir, "package.json");
4559
+ if (fs.existsSync(candidate)) {
4560
+ return candidate;
4561
+ }
4562
+ dir = path.dirname(dir);
4563
+ }
4564
+ throw new Error("Could not find package.json");
4565
+ }
4566
+ function resolveCliVersion() {
4567
+ try {
4568
+ const here = path.dirname(fileURLToPath(import.meta.url));
4569
+ const packageJsonPath = findPackageJson(here);
4570
+ const packageJson = JSON.parse(
4571
+ fs.readFileSync(packageJsonPath, "utf-8")
4572
+ );
4573
+ return packageJson.version ?? "unknown";
4574
+ } catch {
4575
+ return "unknown";
4576
+ }
4577
+ }
4578
+ var VERSION = resolveCliVersion();
4579
+
4580
+ // src/cli/index.ts
4581
+ program.name("trail").description("Leave a trail of your work for others to follow").version(VERSION).option(
4582
+ "--data-dir <path>",
4583
+ "Override trajectory storage directory (or set TRAJECTORIES_DATA_DIR)"
4584
+ ).hook("preAction", (thisCommand) => {
4585
+ const opts = thisCommand.opts();
4586
+ if (opts.dataDir) {
4587
+ process.env.TRAJECTORIES_DATA_DIR = opts.dataDir;
4588
+ }
4589
+ });
4590
+ registerCommands(program);
4591
+ program.parse();
4592
+ //# sourceMappingURL=index.js.map