opencode-swarm-plugin 0.35.0 → 0.36.1

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 (52) hide show
  1. package/.hive/issues.jsonl +4 -4
  2. package/.hive/memories.jsonl +274 -1
  3. package/.turbo/turbo-build.log +4 -4
  4. package/.turbo/turbo-test.log +307 -307
  5. package/CHANGELOG.md +133 -0
  6. package/bin/swarm.ts +234 -179
  7. package/dist/compaction-hook.d.ts +54 -4
  8. package/dist/compaction-hook.d.ts.map +1 -1
  9. package/dist/eval-capture.d.ts +122 -17
  10. package/dist/eval-capture.d.ts.map +1 -1
  11. package/dist/index.d.ts +1 -7
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1278 -619
  14. package/dist/planning-guardrails.d.ts +121 -0
  15. package/dist/planning-guardrails.d.ts.map +1 -1
  16. package/dist/plugin.d.ts +9 -9
  17. package/dist/plugin.d.ts.map +1 -1
  18. package/dist/plugin.js +1283 -329
  19. package/dist/schemas/task.d.ts +0 -1
  20. package/dist/schemas/task.d.ts.map +1 -1
  21. package/dist/swarm-decompose.d.ts +0 -8
  22. package/dist/swarm-decompose.d.ts.map +1 -1
  23. package/dist/swarm-orchestrate.d.ts.map +1 -1
  24. package/dist/swarm-prompts.d.ts +0 -4
  25. package/dist/swarm-prompts.d.ts.map +1 -1
  26. package/dist/swarm-review.d.ts.map +1 -1
  27. package/dist/swarm.d.ts +0 -6
  28. package/dist/swarm.d.ts.map +1 -1
  29. package/evals/README.md +38 -0
  30. package/evals/coordinator-session.eval.ts +154 -0
  31. package/evals/fixtures/coordinator-sessions.ts +328 -0
  32. package/evals/lib/data-loader.ts +69 -0
  33. package/evals/scorers/coordinator-discipline.evalite-test.ts +536 -0
  34. package/evals/scorers/coordinator-discipline.ts +315 -0
  35. package/evals/scorers/index.ts +12 -0
  36. package/examples/plugin-wrapper-template.ts +747 -34
  37. package/package.json +2 -2
  38. package/src/compaction-hook.test.ts +234 -281
  39. package/src/compaction-hook.ts +221 -63
  40. package/src/eval-capture.test.ts +390 -0
  41. package/src/eval-capture.ts +168 -10
  42. package/src/index.ts +89 -2
  43. package/src/learning.integration.test.ts +0 -2
  44. package/src/planning-guardrails.test.ts +387 -2
  45. package/src/planning-guardrails.ts +289 -0
  46. package/src/plugin.ts +10 -10
  47. package/src/schemas/task.ts +0 -1
  48. package/src/swarm-decompose.ts +21 -8
  49. package/src/swarm-orchestrate.ts +44 -0
  50. package/src/swarm-prompts.ts +20 -0
  51. package/src/swarm-review.ts +41 -0
  52. package/src/swarm.integration.test.ts +0 -40
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Tests for eval-capture coordinator event schemas and session capture
3
+ */
4
+ import { type Mock, afterEach, beforeEach, describe, expect, test } from "bun:test";
5
+ import * as fs from "node:fs";
6
+ import * as os from "node:os";
7
+ import * as path from "node:path";
8
+ import {
9
+ type CoordinatorEvent,
10
+ CoordinatorEventSchema,
11
+ type CoordinatorSession,
12
+ CoordinatorSessionSchema,
13
+ captureCoordinatorEvent,
14
+ saveSession,
15
+ } from "./eval-capture.js";
16
+
17
+ describe("CoordinatorEvent schemas", () => {
18
+ describe("DECISION events", () => {
19
+ test("validates strategy_selected event", () => {
20
+ const event: CoordinatorEvent = {
21
+ session_id: "test-session",
22
+ epic_id: "bd-123",
23
+ timestamp: new Date().toISOString(),
24
+ event_type: "DECISION",
25
+ decision_type: "strategy_selected",
26
+ payload: {
27
+ strategy: "file-based",
28
+ reasoning: "Files are well isolated",
29
+ },
30
+ };
31
+
32
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
33
+ });
34
+
35
+ test("validates worker_spawned event", () => {
36
+ const event: CoordinatorEvent = {
37
+ session_id: "test-session",
38
+ epic_id: "bd-123",
39
+ timestamp: new Date().toISOString(),
40
+ event_type: "DECISION",
41
+ decision_type: "worker_spawned",
42
+ payload: {
43
+ worker_id: "GreenStorm",
44
+ subtask_id: "bd-123.1",
45
+ files: ["src/test.ts"],
46
+ },
47
+ };
48
+
49
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
50
+ });
51
+
52
+ test("validates review_completed event", () => {
53
+ const event: CoordinatorEvent = {
54
+ session_id: "test-session",
55
+ epic_id: "bd-123",
56
+ timestamp: new Date().toISOString(),
57
+ event_type: "DECISION",
58
+ decision_type: "review_completed",
59
+ payload: {
60
+ subtask_id: "bd-123.1",
61
+ approved: true,
62
+ issues_found: 0,
63
+ },
64
+ };
65
+
66
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
67
+ });
68
+
69
+ test("validates decomposition_complete event", () => {
70
+ const event: CoordinatorEvent = {
71
+ session_id: "test-session",
72
+ epic_id: "bd-123",
73
+ timestamp: new Date().toISOString(),
74
+ event_type: "DECISION",
75
+ decision_type: "decomposition_complete",
76
+ payload: {
77
+ subtask_count: 3,
78
+ strategy: "feature-based",
79
+ },
80
+ };
81
+
82
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
83
+ });
84
+ });
85
+
86
+ describe("VIOLATION events", () => {
87
+ test("validates coordinator_edited_file event", () => {
88
+ const event: CoordinatorEvent = {
89
+ session_id: "test-session",
90
+ epic_id: "bd-123",
91
+ timestamp: new Date().toISOString(),
92
+ event_type: "VIOLATION",
93
+ violation_type: "coordinator_edited_file",
94
+ payload: {
95
+ file: "src/bad.ts",
96
+ operation: "edit",
97
+ },
98
+ };
99
+
100
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
101
+ });
102
+
103
+ test("validates coordinator_ran_tests event", () => {
104
+ const event: CoordinatorEvent = {
105
+ session_id: "test-session",
106
+ epic_id: "bd-123",
107
+ timestamp: new Date().toISOString(),
108
+ event_type: "VIOLATION",
109
+ violation_type: "coordinator_ran_tests",
110
+ payload: {
111
+ command: "bun test",
112
+ },
113
+ };
114
+
115
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
116
+ });
117
+
118
+ test("validates coordinator_reserved_files event", () => {
119
+ const event: CoordinatorEvent = {
120
+ session_id: "test-session",
121
+ epic_id: "bd-123",
122
+ timestamp: new Date().toISOString(),
123
+ event_type: "VIOLATION",
124
+ violation_type: "coordinator_reserved_files",
125
+ payload: {
126
+ files: ["src/auth.ts"],
127
+ },
128
+ };
129
+
130
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
131
+ });
132
+
133
+ test("validates no_worker_spawned event", () => {
134
+ const event: CoordinatorEvent = {
135
+ session_id: "test-session",
136
+ epic_id: "bd-123",
137
+ timestamp: new Date().toISOString(),
138
+ event_type: "VIOLATION",
139
+ violation_type: "no_worker_spawned",
140
+ payload: {
141
+ subtask_id: "bd-123.1",
142
+ reason: "Did work directly",
143
+ },
144
+ };
145
+
146
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
147
+ });
148
+ });
149
+
150
+ describe("OUTCOME events", () => {
151
+ test("validates subtask_success event", () => {
152
+ const event: CoordinatorEvent = {
153
+ session_id: "test-session",
154
+ epic_id: "bd-123",
155
+ timestamp: new Date().toISOString(),
156
+ event_type: "OUTCOME",
157
+ outcome_type: "subtask_success",
158
+ payload: {
159
+ subtask_id: "bd-123.1",
160
+ duration_ms: 45000,
161
+ files_touched: ["src/auth.ts"],
162
+ },
163
+ };
164
+
165
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
166
+ });
167
+
168
+ test("validates subtask_retry event", () => {
169
+ const event: CoordinatorEvent = {
170
+ session_id: "test-session",
171
+ epic_id: "bd-123",
172
+ timestamp: new Date().toISOString(),
173
+ event_type: "OUTCOME",
174
+ outcome_type: "subtask_retry",
175
+ payload: {
176
+ subtask_id: "bd-123.1",
177
+ retry_count: 2,
178
+ reason: "Review rejected",
179
+ },
180
+ };
181
+
182
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
183
+ });
184
+
185
+ test("validates subtask_failed event", () => {
186
+ const event: CoordinatorEvent = {
187
+ session_id: "test-session",
188
+ epic_id: "bd-123",
189
+ timestamp: new Date().toISOString(),
190
+ event_type: "OUTCOME",
191
+ outcome_type: "subtask_failed",
192
+ payload: {
193
+ subtask_id: "bd-123.1",
194
+ error: "Type error in auth.ts",
195
+ },
196
+ };
197
+
198
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
199
+ });
200
+
201
+ test("validates epic_complete event", () => {
202
+ const event: CoordinatorEvent = {
203
+ session_id: "test-session",
204
+ epic_id: "bd-123",
205
+ timestamp: new Date().toISOString(),
206
+ event_type: "OUTCOME",
207
+ outcome_type: "epic_complete",
208
+ payload: {
209
+ success: true,
210
+ total_duration_ms: 180000,
211
+ subtasks_completed: 3,
212
+ },
213
+ };
214
+
215
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
216
+ });
217
+ });
218
+ });
219
+
220
+ describe("CoordinatorSession schema", () => {
221
+ test("validates complete session", () => {
222
+ const session: CoordinatorSession = {
223
+ session_id: "test-session",
224
+ epic_id: "bd-123",
225
+ start_time: new Date().toISOString(),
226
+ end_time: new Date().toISOString(),
227
+ events: [
228
+ {
229
+ session_id: "test-session",
230
+ epic_id: "bd-123",
231
+ timestamp: new Date().toISOString(),
232
+ event_type: "DECISION",
233
+ decision_type: "strategy_selected",
234
+ payload: { strategy: "file-based" },
235
+ },
236
+ ],
237
+ };
238
+
239
+ expect(() => CoordinatorSessionSchema.parse(session)).not.toThrow();
240
+ });
241
+
242
+ test("validates session without end_time", () => {
243
+ const session: Partial<CoordinatorSession> = {
244
+ session_id: "test-session",
245
+ epic_id: "bd-123",
246
+ start_time: new Date().toISOString(),
247
+ events: [],
248
+ };
249
+
250
+ expect(() => CoordinatorSessionSchema.parse(session)).not.toThrow();
251
+ });
252
+ });
253
+
254
+ describe("captureCoordinatorEvent", () => {
255
+ let sessionDir: string;
256
+ let sessionId: string;
257
+
258
+ beforeEach(() => {
259
+ sessionDir = path.join(os.homedir(), ".config", "swarm-tools", "sessions");
260
+ sessionId = `test-${Date.now()}`;
261
+ });
262
+
263
+ afterEach(() => {
264
+ // Clean up test session file
265
+ const sessionPath = path.join(sessionDir, `${sessionId}.jsonl`);
266
+ if (fs.existsSync(sessionPath)) {
267
+ fs.unlinkSync(sessionPath);
268
+ }
269
+ });
270
+
271
+ test("creates session directory if not exists", () => {
272
+ const event: CoordinatorEvent = {
273
+ session_id: sessionId,
274
+ epic_id: "bd-123",
275
+ timestamp: new Date().toISOString(),
276
+ event_type: "DECISION",
277
+ decision_type: "strategy_selected",
278
+ payload: { strategy: "file-based" },
279
+ };
280
+
281
+ captureCoordinatorEvent(event);
282
+
283
+ expect(fs.existsSync(sessionDir)).toBe(true);
284
+ });
285
+
286
+ test("appends event to session file", () => {
287
+ const event: CoordinatorEvent = {
288
+ session_id: sessionId,
289
+ epic_id: "bd-123",
290
+ timestamp: new Date().toISOString(),
291
+ event_type: "DECISION",
292
+ decision_type: "strategy_selected",
293
+ payload: { strategy: "file-based" },
294
+ };
295
+
296
+ captureCoordinatorEvent(event);
297
+
298
+ const sessionPath = path.join(sessionDir, `${sessionId}.jsonl`);
299
+ expect(fs.existsSync(sessionPath)).toBe(true);
300
+
301
+ const content = fs.readFileSync(sessionPath, "utf-8");
302
+ const lines = content.trim().split("\n");
303
+ expect(lines).toHaveLength(1);
304
+
305
+ const parsed = JSON.parse(lines[0]);
306
+ expect(parsed.session_id).toBe(sessionId);
307
+ expect(parsed.event_type).toBe("DECISION");
308
+ });
309
+
310
+ test("appends multiple events to same session", () => {
311
+ const event1: CoordinatorEvent = {
312
+ session_id: sessionId,
313
+ epic_id: "bd-123",
314
+ timestamp: new Date().toISOString(),
315
+ event_type: "DECISION",
316
+ decision_type: "strategy_selected",
317
+ payload: { strategy: "file-based" },
318
+ };
319
+
320
+ const event2: CoordinatorEvent = {
321
+ session_id: sessionId,
322
+ epic_id: "bd-123",
323
+ timestamp: new Date().toISOString(),
324
+ event_type: "VIOLATION",
325
+ violation_type: "coordinator_edited_file",
326
+ payload: { file: "src/bad.ts" },
327
+ };
328
+
329
+ captureCoordinatorEvent(event1);
330
+ captureCoordinatorEvent(event2);
331
+
332
+ const sessionPath = path.join(sessionDir, `${sessionId}.jsonl`);
333
+ const content = fs.readFileSync(sessionPath, "utf-8");
334
+ const lines = content.trim().split("\n");
335
+ expect(lines).toHaveLength(2);
336
+ });
337
+ });
338
+
339
+ describe("saveSession", () => {
340
+ let sessionDir: string;
341
+ let sessionId: string;
342
+
343
+ beforeEach(() => {
344
+ sessionDir = path.join(os.homedir(), ".config", "swarm-tools", "sessions");
345
+ sessionId = `test-${Date.now()}`;
346
+ });
347
+
348
+ afterEach(() => {
349
+ // Clean up test session file
350
+ const sessionPath = path.join(sessionDir, `${sessionId}.jsonl`);
351
+ if (fs.existsSync(sessionPath)) {
352
+ fs.unlinkSync(sessionPath);
353
+ }
354
+ });
355
+
356
+ test("wraps events in session structure", () => {
357
+ // Capture some events
358
+ const event1: CoordinatorEvent = {
359
+ session_id: sessionId,
360
+ epic_id: "bd-123",
361
+ timestamp: new Date().toISOString(),
362
+ event_type: "DECISION",
363
+ decision_type: "strategy_selected",
364
+ payload: { strategy: "file-based" },
365
+ };
366
+
367
+ captureCoordinatorEvent(event1);
368
+
369
+ // Save session
370
+ const session = saveSession({
371
+ session_id: sessionId,
372
+ epic_id: "bd-123",
373
+ });
374
+
375
+ expect(session).toBeDefined();
376
+ expect(session.session_id).toBe(sessionId);
377
+ expect(session.events).toHaveLength(1);
378
+ expect(session.start_time).toBeDefined();
379
+ expect(session.end_time).toBeDefined();
380
+ });
381
+
382
+ test("returns null if session file does not exist", () => {
383
+ const session = saveSession({
384
+ session_id: "nonexistent",
385
+ epic_id: "bd-999",
386
+ });
387
+
388
+ expect(session).toBeNull();
389
+ });
390
+ });
@@ -9,12 +9,15 @@
9
9
  * 2. swarm_complete captures: outcome signals per subtask
10
10
  * 3. swarm_record_outcome captures: learning signals
11
11
  * 4. Human feedback (optional): accept/reject/modify
12
+ * 5. Coordinator events: decisions, violations, outcomes
13
+ * 6. Session capture: full coordinator session to ~/.config/swarm-tools/sessions/
12
14
  *
13
15
  * @module eval-capture
14
16
  */
17
+ import * as fs from "node:fs";
18
+ import * as os from "node:os";
19
+ import * as path from "node:path";
15
20
  import { z } from "zod";
16
- import * as fs from "fs";
17
- import * as path from "path";
18
21
 
19
22
  // ============================================================================
20
23
  // Schemas
@@ -63,8 +66,8 @@ export const EvalRecordSchema = z.object({
63
66
  context: z.string().optional(),
64
67
  /** Strategy used for decomposition */
65
68
  strategy: z.enum(["file-based", "feature-based", "risk-based", "auto"]),
66
- /** Max subtasks requested */
67
- max_subtasks: z.number().int().min(1).max(10),
69
+ /** Number of subtasks generated */
70
+ subtask_count: z.number().int().min(1),
68
71
 
69
72
  // OUTPUT (the decomposition)
70
73
  /** Epic title */
@@ -119,6 +122,67 @@ export type PartialEvalRecord = Partial<EvalRecord> & {
119
122
  task: string;
120
123
  };
121
124
 
125
+ /**
126
+ * Coordinator Event - captures coordinator decisions, violations, and outcomes
127
+ */
128
+ export const CoordinatorEventSchema = z.discriminatedUnion("event_type", [
129
+ // DECISION events
130
+ z.object({
131
+ session_id: z.string(),
132
+ epic_id: z.string(),
133
+ timestamp: z.string(),
134
+ event_type: z.literal("DECISION"),
135
+ decision_type: z.enum([
136
+ "strategy_selected",
137
+ "worker_spawned",
138
+ "review_completed",
139
+ "decomposition_complete",
140
+ ]),
141
+ payload: z.any(),
142
+ }),
143
+ // VIOLATION events
144
+ z.object({
145
+ session_id: z.string(),
146
+ epic_id: z.string(),
147
+ timestamp: z.string(),
148
+ event_type: z.literal("VIOLATION"),
149
+ violation_type: z.enum([
150
+ "coordinator_edited_file",
151
+ "coordinator_ran_tests",
152
+ "coordinator_reserved_files",
153
+ "no_worker_spawned",
154
+ ]),
155
+ payload: z.any(),
156
+ }),
157
+ // OUTCOME events
158
+ z.object({
159
+ session_id: z.string(),
160
+ epic_id: z.string(),
161
+ timestamp: z.string(),
162
+ event_type: z.literal("OUTCOME"),
163
+ outcome_type: z.enum([
164
+ "subtask_success",
165
+ "subtask_retry",
166
+ "subtask_failed",
167
+ "epic_complete",
168
+ ]),
169
+ payload: z.any(),
170
+ }),
171
+ ]);
172
+ export type CoordinatorEvent = z.infer<typeof CoordinatorEventSchema>;
173
+
174
+ /**
175
+ * Coordinator Session - wraps a full coordinator session
176
+ */
177
+ export const CoordinatorSessionSchema = z.object({
178
+ session_id: z.string(),
179
+ epic_id: z.string(),
180
+ start_time: z.string(),
181
+ end_time: z.string().optional(),
182
+ events: z.array(CoordinatorEventSchema),
183
+ });
184
+ export type CoordinatorSession = z.infer<typeof CoordinatorSessionSchema>;
185
+
122
186
  // ============================================================================
123
187
  // Storage
124
188
  // ============================================================================
@@ -155,7 +219,7 @@ export function appendEvalRecord(
155
219
  ): void {
156
220
  ensureEvalDataDir(projectPath);
157
221
  const evalPath = getEvalDataPath(projectPath);
158
- const line = JSON.stringify(record) + "\n";
222
+ const line = `${JSON.stringify(record)}\n`;
159
223
  fs.appendFileSync(evalPath, line, "utf-8");
160
224
  }
161
225
 
@@ -211,7 +275,7 @@ export function updateEvalRecord(
211
275
 
212
276
  // Rewrite the file
213
277
  const evalPath = getEvalDataPath(projectPath);
214
- const content = records.map((r) => JSON.stringify(r)).join("\n") + "\n";
278
+ const content = `${records.map((r) => JSON.stringify(r)).join("\n")}\n`;
215
279
  fs.writeFileSync(evalPath, content, "utf-8");
216
280
 
217
281
  return true;
@@ -238,7 +302,6 @@ export function captureDecomposition(params: {
238
302
  task: string;
239
303
  context?: string;
240
304
  strategy: "file-based" | "feature-based" | "risk-based" | "auto";
241
- maxSubtasks: number;
242
305
  epicTitle: string;
243
306
  epicDescription?: string;
244
307
  subtasks: Array<{
@@ -256,7 +319,7 @@ export function captureDecomposition(params: {
256
319
  task: params.task,
257
320
  context: params.context,
258
321
  strategy: params.strategy,
259
- max_subtasks: params.maxSubtasks,
322
+ subtask_count: params.subtasks.length,
260
323
  epic_title: params.epicTitle,
261
324
  epic_description: params.epicDescription,
262
325
  subtasks: params.subtasks,
@@ -409,7 +472,7 @@ export function exportForEvalite(projectPath: string): Array<{
409
472
  input: { task: string; context?: string };
410
473
  expected: {
411
474
  minSubtasks: number;
412
- maxSubtasks: number;
475
+ subtaskCount: number;
413
476
  requiredFiles?: string[];
414
477
  overallSuccess?: boolean;
415
478
  };
@@ -426,7 +489,7 @@ export function exportForEvalite(projectPath: string): Array<{
426
489
  },
427
490
  expected: {
428
491
  minSubtasks: 2,
429
- maxSubtasks: record.max_subtasks,
492
+ subtaskCount: record.subtask_count,
430
493
  requiredFiles: record.subtasks.flatMap((s) => s.files),
431
494
  overallSuccess: record.overall_success,
432
495
  },
@@ -485,3 +548,98 @@ export function getEvalDataStats(projectPath: string): {
485
548
  avgTimeBalance,
486
549
  };
487
550
  }
551
+
552
+ // ============================================================================
553
+ // Coordinator Session Capture
554
+ // ============================================================================
555
+
556
+ /**
557
+ * Get the session directory path
558
+ */
559
+ export function getSessionDir(): string {
560
+ return path.join(os.homedir(), ".config", "swarm-tools", "sessions");
561
+ }
562
+
563
+ /**
564
+ * Get the session file path for a session ID
565
+ */
566
+ export function getSessionPath(sessionId: string): string {
567
+ return path.join(getSessionDir(), `${sessionId}.jsonl`);
568
+ }
569
+
570
+ /**
571
+ * Ensure the session directory exists
572
+ */
573
+ export function ensureSessionDir(): void {
574
+ const sessionDir = getSessionDir();
575
+ if (!fs.existsSync(sessionDir)) {
576
+ fs.mkdirSync(sessionDir, { recursive: true });
577
+ }
578
+ }
579
+
580
+ /**
581
+ * Capture a coordinator event to the session file
582
+ *
583
+ * Appends the event as a JSONL line to ~/.config/swarm-tools/sessions/{session_id}.jsonl
584
+ */
585
+ export function captureCoordinatorEvent(event: CoordinatorEvent): void {
586
+ // Validate event
587
+ CoordinatorEventSchema.parse(event);
588
+
589
+ // Ensure directory exists
590
+ ensureSessionDir();
591
+
592
+ // Append to session file
593
+ const sessionPath = getSessionPath(event.session_id);
594
+ const line = `${JSON.stringify(event)}\n`;
595
+ fs.appendFileSync(sessionPath, line, "utf-8");
596
+ }
597
+
598
+ /**
599
+ * Read all events from a session file
600
+ */
601
+ export function readSessionEvents(sessionId: string): CoordinatorEvent[] {
602
+ const sessionPath = getSessionPath(sessionId);
603
+ if (!fs.existsSync(sessionPath)) {
604
+ return [];
605
+ }
606
+
607
+ const content = fs.readFileSync(sessionPath, "utf-8");
608
+ const lines = content.trim().split("\n").filter(Boolean);
609
+
610
+ return lines.map((line) => {
611
+ const parsed = JSON.parse(line);
612
+ return CoordinatorEventSchema.parse(parsed);
613
+ });
614
+ }
615
+
616
+ /**
617
+ * Save a session - wraps all events in a CoordinatorSession structure
618
+ *
619
+ * Reads all events from the session file and wraps them in a session object.
620
+ * Returns null if the session file doesn't exist.
621
+ */
622
+ export function saveSession(params: {
623
+ session_id: string;
624
+ epic_id: string;
625
+ }): CoordinatorSession | null {
626
+ const events = readSessionEvents(params.session_id);
627
+ if (events.length === 0) {
628
+ return null;
629
+ }
630
+
631
+ // Get timestamps from events
632
+ const timestamps = events.map((e) => new Date(e.timestamp).getTime());
633
+ const startTime = new Date(Math.min(...timestamps)).toISOString();
634
+ const endTime = new Date(Math.max(...timestamps)).toISOString();
635
+
636
+ const session: CoordinatorSession = {
637
+ session_id: params.session_id,
638
+ epic_id: params.epic_id,
639
+ start_time: startTime,
640
+ end_time: endTime,
641
+ events,
642
+ };
643
+
644
+ return session;
645
+ }