agent-trajectories 0.1.0

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 (54) hide show
  1. package/.beads/.local_version +1 -0
  2. package/.beads/README.md +81 -0
  3. package/.beads/config.yaml +62 -0
  4. package/.beads/issues.jsonl +0 -0
  5. package/.beads/metadata.json +4 -0
  6. package/.gitattributes +3 -0
  7. package/IMPLEMENTATION-PROPOSAL.md +1598 -0
  8. package/PROPOSAL-trajectories.md +1582 -0
  9. package/README.md +275 -0
  10. package/biome.json +20 -0
  11. package/docs/architecture/core.md +477 -0
  12. package/docs/architecture/memory-integration.md +186 -0
  13. package/docs/architecture/web-viewer.md +213 -0
  14. package/package.json +47 -0
  15. package/src/cli/commands/abandon.ts +32 -0
  16. package/src/cli/commands/complete.ts +51 -0
  17. package/src/cli/commands/decision.ts +49 -0
  18. package/src/cli/commands/export.ts +100 -0
  19. package/src/cli/commands/index.ts +39 -0
  20. package/src/cli/commands/list.ts +90 -0
  21. package/src/cli/commands/show.ts +98 -0
  22. package/src/cli/commands/start.ts +53 -0
  23. package/src/cli/commands/status.ts +68 -0
  24. package/src/cli/index.ts +25 -0
  25. package/src/cli/runner.ts +67 -0
  26. package/src/core/id.ts +62 -0
  27. package/src/core/index.ts +54 -0
  28. package/src/core/schema.ts +272 -0
  29. package/src/core/trajectory.ts +283 -0
  30. package/src/core/types.ts +272 -0
  31. package/src/export/index.ts +10 -0
  32. package/src/export/json.ts +26 -0
  33. package/src/export/markdown.ts +216 -0
  34. package/src/export/pr-summary.ts +57 -0
  35. package/src/export/timeline.ts +69 -0
  36. package/src/index.ts +69 -0
  37. package/src/storage/file.ts +394 -0
  38. package/src/storage/index.ts +6 -0
  39. package/src/storage/interface.ts +92 -0
  40. package/src/web/generator.ts +347 -0
  41. package/src/web/index.ts +6 -0
  42. package/src/web/styles.ts +355 -0
  43. package/src/workspace/index.ts +6 -0
  44. package/src/workspace/storage.ts +231 -0
  45. package/src/workspace/types.ts +88 -0
  46. package/tests/cli/commands.test.ts +476 -0
  47. package/tests/core/trajectory.test.ts +426 -0
  48. package/tests/export/export.test.ts +376 -0
  49. package/tests/storage/storage.test.ts +410 -0
  50. package/tests/web/generator.test.ts +197 -0
  51. package/tests/workspace/storage.test.ts +275 -0
  52. package/tsconfig.json +26 -0
  53. package/tsup.config.ts +14 -0
  54. package/vitest.config.ts +15 -0
@@ -0,0 +1,476 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ /**
7
+ * Test stubs for CLI commands
8
+ *
9
+ * These tests define the expected behavior of the CLI.
10
+ * All tests should FAIL until implementation is complete.
11
+ *
12
+ * Coverage: happy path, edge cases, error scenarios
13
+ * Structure: Arrange-Act-Assert
14
+ */
15
+
16
+ describe("CLI Commands", () => {
17
+ let tempDir: string;
18
+ let originalCwd: string;
19
+
20
+ beforeEach(async () => {
21
+ tempDir = await mkdtemp(join(tmpdir(), "trail-cli-test-"));
22
+ originalCwd = process.cwd();
23
+ process.chdir(tempDir);
24
+ });
25
+
26
+ afterEach(async () => {
27
+ process.chdir(originalCwd);
28
+ await rm(tempDir, { recursive: true, force: true });
29
+ });
30
+
31
+ describe("trail start", () => {
32
+ it("should create a new trajectory", async () => {
33
+ // Arrange
34
+ const { runCommand } = await import("../../src/cli/runner.js");
35
+
36
+ // Act
37
+ const result = await runCommand(["start", "Implement auth"]);
38
+
39
+ // Assert
40
+ expect(result.success).toBe(true);
41
+ expect(result.output).toContain("Trajectory started");
42
+ expect(result.output).toMatch(/traj_[a-z0-9]+/);
43
+ });
44
+
45
+ it("should fail if a trajectory is already active", async () => {
46
+ // Arrange
47
+ const { runCommand } = await import("../../src/cli/runner.js");
48
+ await runCommand(["start", "First task"]);
49
+
50
+ // Act
51
+ const result = await runCommand(["start", "Second task"]);
52
+
53
+ // Assert
54
+ expect(result.success).toBe(false);
55
+ expect(result.error).toContain("already active");
56
+ });
57
+
58
+ it("should support --task flag for external reference", async () => {
59
+ // Arrange
60
+ const { runCommand } = await import("../../src/cli/runner.js");
61
+
62
+ // Act
63
+ const result = await runCommand([
64
+ "start",
65
+ "Fix bug",
66
+ "--task",
67
+ "GH#123",
68
+ "--source",
69
+ "github",
70
+ ]);
71
+
72
+ // Assert
73
+ expect(result.success).toBe(true);
74
+
75
+ // Verify the trajectory has the task reference
76
+ const { FileStorage } = await import("../../src/storage/file.js");
77
+ const storage = new FileStorage(tempDir);
78
+ await storage.initialize();
79
+ const active = await storage.getActive();
80
+ expect(active?.task.source?.id).toBe("GH#123");
81
+ });
82
+ });
83
+
84
+ describe("trail status", () => {
85
+ it("should show active trajectory details", async () => {
86
+ // Arrange
87
+ const { runCommand } = await import("../../src/cli/runner.js");
88
+ await runCommand(["start", "Test task"]);
89
+
90
+ // Act
91
+ const result = await runCommand(["status"]);
92
+
93
+ // Assert
94
+ expect(result.success).toBe(true);
95
+ expect(result.output).toContain("Test task");
96
+ expect(result.output).toContain("active");
97
+ });
98
+
99
+ it("should indicate when no trajectory is active", async () => {
100
+ // Arrange
101
+ const { runCommand } = await import("../../src/cli/runner.js");
102
+
103
+ // Act
104
+ const result = await runCommand(["status"]);
105
+
106
+ // Assert
107
+ expect(result.success).toBe(true);
108
+ expect(result.output).toContain("No active trajectory");
109
+ });
110
+
111
+ it("should show chapter and event counts", async () => {
112
+ // Arrange
113
+ const { runCommand } = await import("../../src/cli/runner.js");
114
+ await runCommand(["start", "Test task"]);
115
+ await runCommand(["decision", "Choice A", "--reasoning", "Because"]);
116
+
117
+ // Act
118
+ const result = await runCommand(["status"]);
119
+
120
+ // Assert
121
+ // Start command creates initial chapter, decision adds an event
122
+ expect(result.output).toMatch(/Chapters:\s+1/);
123
+ expect(result.output).toMatch(/Events:\s+1/);
124
+ });
125
+ });
126
+
127
+ describe("trail decision", () => {
128
+ it("should record a decision in the active trajectory", async () => {
129
+ // Arrange
130
+ const { runCommand } = await import("../../src/cli/runner.js");
131
+ await runCommand(["start", "Test task"]);
132
+
133
+ // Act
134
+ const result = await runCommand([
135
+ "decision",
136
+ "Chose JWT",
137
+ "--reasoning",
138
+ "Stateless scaling",
139
+ "--alternatives",
140
+ "Sessions,OAuth",
141
+ ]);
142
+
143
+ // Assert
144
+ expect(result.success).toBe(true);
145
+ expect(result.output).toContain("Decision recorded");
146
+ });
147
+
148
+ it("should fail without an active trajectory", async () => {
149
+ // Arrange
150
+ const { runCommand } = await import("../../src/cli/runner.js");
151
+
152
+ // Act
153
+ const result = await runCommand([
154
+ "decision",
155
+ "Choice",
156
+ "--reasoning",
157
+ "Why",
158
+ ]);
159
+
160
+ // Assert
161
+ expect(result.success).toBe(false);
162
+ expect(result.error).toContain("No active trajectory");
163
+ });
164
+
165
+ it("should allow decisions without reasoning (optional)", async () => {
166
+ // Arrange
167
+ const { runCommand } = await import("../../src/cli/runner.js");
168
+ await runCommand(["start", "Test task"]);
169
+
170
+ // Act - reasoning is now optional for minor decisions
171
+ const result = await runCommand(["decision", "Minor choice"]);
172
+
173
+ // Assert
174
+ expect(result.success).toBe(true);
175
+ expect(result.output).toContain("Decision recorded");
176
+ });
177
+ });
178
+
179
+ describe("trail complete", () => {
180
+ it("should complete the trajectory with retrospective", async () => {
181
+ // Arrange
182
+ const { runCommand } = await import("../../src/cli/runner.js");
183
+ await runCommand(["start", "Test task"]);
184
+
185
+ // Mock the interactive prompt
186
+ vi.mock("@clack/prompts", () => ({
187
+ text: vi.fn().mockResolvedValue("Summary of work"),
188
+ confirm: vi.fn().mockResolvedValue(true),
189
+ }));
190
+
191
+ // Act
192
+ const result = await runCommand([
193
+ "complete",
194
+ "--summary",
195
+ "Implemented feature",
196
+ "--confidence",
197
+ "0.85",
198
+ ]);
199
+
200
+ // Assert
201
+ expect(result.success).toBe(true);
202
+ expect(result.output).toContain("Trajectory completed");
203
+ });
204
+
205
+ it("should fail without an active trajectory", async () => {
206
+ // Arrange
207
+ const { runCommand } = await import("../../src/cli/runner.js");
208
+
209
+ // Act
210
+ const result = await runCommand([
211
+ "complete",
212
+ "--summary",
213
+ "Done",
214
+ "--confidence",
215
+ "0.9",
216
+ ]);
217
+
218
+ // Assert
219
+ expect(result.success).toBe(false);
220
+ expect(result.error).toContain("No active trajectory");
221
+ });
222
+ });
223
+
224
+ describe("trail abandon", () => {
225
+ it("should abandon the active trajectory", async () => {
226
+ // Arrange
227
+ const { runCommand } = await import("../../src/cli/runner.js");
228
+ await runCommand(["start", "Test task"]);
229
+
230
+ // Act
231
+ const result = await runCommand(["abandon", "--reason", "Requirements changed"]);
232
+
233
+ // Assert
234
+ expect(result.success).toBe(true);
235
+ expect(result.output).toContain("Trajectory abandoned");
236
+ });
237
+ });
238
+
239
+ describe("trail list", () => {
240
+ it("should list trajectories", async () => {
241
+ // Arrange
242
+ const { runCommand } = await import("../../src/cli/runner.js");
243
+ await runCommand(["start", "Task 1"]);
244
+ await runCommand([
245
+ "complete",
246
+ "--summary",
247
+ "Done",
248
+ "--confidence",
249
+ "0.9",
250
+ ]);
251
+ await runCommand(["start", "Task 2"]);
252
+
253
+ // Act
254
+ const result = await runCommand(["list"]);
255
+
256
+ // Assert
257
+ expect(result.success).toBe(true);
258
+ expect(result.output).toContain("Task 1");
259
+ expect(result.output).toContain("Task 2");
260
+ });
261
+
262
+ it("should filter by status", async () => {
263
+ // Arrange
264
+ const { runCommand } = await import("../../src/cli/runner.js");
265
+ await runCommand(["start", "Task 1"]);
266
+ await runCommand([
267
+ "complete",
268
+ "--summary",
269
+ "Done",
270
+ "--confidence",
271
+ "0.9",
272
+ ]);
273
+ await runCommand(["start", "Task 2"]);
274
+
275
+ // Act
276
+ const result = await runCommand(["list", "--status", "completed"]);
277
+
278
+ // Assert
279
+ expect(result.output).toContain("Task 1");
280
+ expect(result.output).not.toContain("Task 2");
281
+ });
282
+
283
+ it("should limit results", async () => {
284
+ // Arrange
285
+ const { runCommand } = await import("../../src/cli/runner.js");
286
+ for (let i = 1; i <= 5; i++) {
287
+ await runCommand(["start", `Task ${i}`]);
288
+ await runCommand([
289
+ "complete",
290
+ "--summary",
291
+ "Done",
292
+ "--confidence",
293
+ "0.9",
294
+ ]);
295
+ }
296
+
297
+ // Act
298
+ const result = await runCommand(["list", "--limit", "3"]);
299
+
300
+ // Assert
301
+ const lines = result.output.split("\n").filter((l) => l.includes("Task"));
302
+ expect(lines.length).toBeLessThanOrEqual(3);
303
+ });
304
+
305
+ it("should search trajectories with --search flag", async () => {
306
+ // Arrange
307
+ const { runCommand } = await import("../../src/cli/runner.js");
308
+ await runCommand(["start", "Implement authentication"]);
309
+ await runCommand([
310
+ "complete",
311
+ "--summary",
312
+ "Added JWT auth",
313
+ "--confidence",
314
+ "0.9",
315
+ ]);
316
+ await runCommand(["start", "Fix database bug"]);
317
+ await runCommand([
318
+ "complete",
319
+ "--summary",
320
+ "Fixed query",
321
+ "--confidence",
322
+ "0.9",
323
+ ]);
324
+
325
+ // Act
326
+ const result = await runCommand(["list", "--search", "auth"]);
327
+
328
+ // Assert
329
+ expect(result.success).toBe(true);
330
+ expect(result.output).toContain("authentication");
331
+ expect(result.output).not.toContain("database");
332
+ });
333
+ });
334
+
335
+ describe("trail show", () => {
336
+ it("should show trajectory details", async () => {
337
+ // Arrange
338
+ const { runCommand } = await import("../../src/cli/runner.js");
339
+ const startResult = await runCommand(["start", "Test task"]);
340
+ const idMatch = startResult.output.match(/traj_[a-z0-9]+/);
341
+ const id = idMatch?.[0];
342
+
343
+ // Act
344
+ const result = await runCommand(["show", id!]);
345
+
346
+ // Assert
347
+ expect(result.success).toBe(true);
348
+ expect(result.output).toContain("Test task");
349
+ });
350
+
351
+ it("should show decisions with --decisions flag", async () => {
352
+ // Arrange
353
+ const { runCommand } = await import("../../src/cli/runner.js");
354
+ await runCommand(["start", "Test task"]);
355
+ await runCommand([
356
+ "decision",
357
+ "Chose A",
358
+ "--reasoning",
359
+ "Because",
360
+ "--alternatives",
361
+ "B,C",
362
+ ]);
363
+ const status = await runCommand(["status"]);
364
+ const idMatch = status.output.match(/traj_[a-z0-9]+/);
365
+ const id = idMatch?.[0];
366
+
367
+ // Act
368
+ const result = await runCommand(["show", id!, "--decisions"]);
369
+
370
+ // Assert
371
+ expect(result.output).toContain("Chose A");
372
+ expect(result.output).toContain("Because");
373
+ });
374
+
375
+ it("should fail for non-existent trajectory", async () => {
376
+ // Arrange
377
+ const { runCommand } = await import("../../src/cli/runner.js");
378
+
379
+ // Act
380
+ const result = await runCommand(["show", "traj_nonexistent"]);
381
+
382
+ // Assert
383
+ expect(result.success).toBe(false);
384
+ expect(result.error).toContain("not found");
385
+ });
386
+ });
387
+
388
+ describe("trail export", () => {
389
+ it("should export trajectory as markdown", async () => {
390
+ // Arrange
391
+ const { runCommand } = await import("../../src/cli/runner.js");
392
+ await runCommand(["start", "Test task"]);
393
+ await runCommand([
394
+ "decision",
395
+ "Chose A",
396
+ "--reasoning",
397
+ "Because",
398
+ ]);
399
+ await runCommand([
400
+ "complete",
401
+ "--summary",
402
+ "Done",
403
+ "--confidence",
404
+ "0.9",
405
+ ]);
406
+ const list = await runCommand(["list"]);
407
+ const idMatch = list.output.match(/traj_[a-z0-9]+/);
408
+ const id = idMatch?.[0];
409
+
410
+ // Act
411
+ const result = await runCommand(["export", id!, "--format", "md"]);
412
+
413
+ // Assert
414
+ expect(result.success).toBe(true);
415
+ expect(result.output).toContain("# Trajectory:");
416
+ expect(result.output).toContain("## Summary");
417
+ });
418
+
419
+ it("should export trajectory as JSON", async () => {
420
+ // Arrange
421
+ const { runCommand } = await import("../../src/cli/runner.js");
422
+ await runCommand(["start", "Test task"]);
423
+ await runCommand([
424
+ "complete",
425
+ "--summary",
426
+ "Done",
427
+ "--confidence",
428
+ "0.9",
429
+ ]);
430
+ const list = await runCommand(["list"]);
431
+ const idMatch = list.output.match(/traj_[a-z0-9]+/);
432
+ const id = idMatch?.[0];
433
+
434
+ // Act
435
+ const result = await runCommand(["export", id!, "--format", "json"]);
436
+
437
+ // Assert
438
+ expect(result.success).toBe(true);
439
+ const parsed = JSON.parse(result.output);
440
+ expect(parsed.id).toBe(id);
441
+ });
442
+
443
+ it("should write to file with --output flag", async () => {
444
+ // Arrange
445
+ const { runCommand } = await import("../../src/cli/runner.js");
446
+ await runCommand(["start", "Test task"]);
447
+ await runCommand([
448
+ "complete",
449
+ "--summary",
450
+ "Done",
451
+ "--confidence",
452
+ "0.9",
453
+ ]);
454
+ const list = await runCommand(["list"]);
455
+ const idMatch = list.output.match(/traj_[a-z0-9]+/);
456
+ const id = idMatch?.[0];
457
+ const outputPath = join(tempDir, "export.md");
458
+
459
+ // Act
460
+ const result = await runCommand([
461
+ "export",
462
+ id!,
463
+ "--format",
464
+ "md",
465
+ "--output",
466
+ outputPath,
467
+ ]);
468
+
469
+ // Assert
470
+ expect(result.success).toBe(true);
471
+ const { readFileSync } = await import("node:fs");
472
+ const content = readFileSync(outputPath, "utf-8");
473
+ expect(content).toContain("# Trajectory:");
474
+ });
475
+ });
476
+ });