opencode-swarm-plugin 0.20.0 → 0.22.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 (41) hide show
  1. package/.beads/issues.jsonl +213 -0
  2. package/INTEGRATION_EXAMPLE.md +66 -0
  3. package/README.md +352 -522
  4. package/dist/index.js +2046 -984
  5. package/dist/plugin.js +2051 -1017
  6. package/docs/analysis/subagent-coordination-patterns.md +2 -0
  7. package/docs/semantic-memory-cli-syntax.md +123 -0
  8. package/docs/swarm-mail-architecture.md +1147 -0
  9. package/evals/README.md +116 -0
  10. package/evals/evalite.config.ts +15 -0
  11. package/evals/example.eval.ts +32 -0
  12. package/evals/fixtures/decomposition-cases.ts +105 -0
  13. package/evals/lib/data-loader.test.ts +288 -0
  14. package/evals/lib/data-loader.ts +111 -0
  15. package/evals/lib/llm.ts +115 -0
  16. package/evals/scorers/index.ts +200 -0
  17. package/evals/scorers/outcome-scorers.test.ts +27 -0
  18. package/evals/scorers/outcome-scorers.ts +349 -0
  19. package/evals/swarm-decomposition.eval.ts +112 -0
  20. package/package.json +8 -1
  21. package/scripts/cleanup-test-memories.ts +346 -0
  22. package/src/beads.ts +49 -0
  23. package/src/eval-capture.ts +487 -0
  24. package/src/index.ts +45 -3
  25. package/src/learning.integration.test.ts +19 -4
  26. package/src/output-guardrails.test.ts +438 -0
  27. package/src/output-guardrails.ts +381 -0
  28. package/src/schemas/index.ts +18 -0
  29. package/src/schemas/swarm-context.ts +115 -0
  30. package/src/storage.ts +117 -5
  31. package/src/streams/events.test.ts +296 -0
  32. package/src/streams/events.ts +93 -0
  33. package/src/streams/migrations.test.ts +24 -20
  34. package/src/streams/migrations.ts +51 -0
  35. package/src/streams/projections.ts +187 -0
  36. package/src/streams/store.ts +275 -0
  37. package/src/swarm-orchestrate.ts +771 -189
  38. package/src/swarm-prompts.ts +84 -12
  39. package/src/swarm.integration.test.ts +124 -0
  40. package/vitest.integration.config.ts +6 -0
  41. package/vitest.integration.setup.ts +48 -0
@@ -21,6 +21,9 @@ import {
21
21
  TaskProgressEventSchema,
22
22
  TaskCompletedEventSchema,
23
23
  TaskBlockedEventSchema,
24
+ DecompositionGeneratedEventSchema,
25
+ SubtaskOutcomeEventSchema,
26
+ HumanFeedbackEventSchema,
24
27
  createEvent,
25
28
  isEventType,
26
29
  type AgentEvent,
@@ -361,6 +364,299 @@ describe("TaskBlockedEventSchema", () => {
361
364
  });
362
365
  });
363
366
 
367
+ describe("DecompositionGeneratedEventSchema", () => {
368
+ it("validates a complete decomposition_generated event", () => {
369
+ const event = {
370
+ type: "decomposition_generated",
371
+ project_key: "/test/project",
372
+ timestamp: Date.now(),
373
+ epic_id: "bd-123",
374
+ task: "Add user authentication",
375
+ context: "OAuth integration for GitHub",
376
+ strategy: "feature-based",
377
+ epic_title: "User Authentication",
378
+ subtasks: [
379
+ {
380
+ title: "Create OAuth flow",
381
+ files: ["src/auth/oauth.ts"],
382
+ priority: 2,
383
+ },
384
+ { title: "Add login UI", files: ["src/ui/login.tsx"], priority: 1 },
385
+ ],
386
+ };
387
+ expect(() => DecompositionGeneratedEventSchema.parse(event)).not.toThrow();
388
+ });
389
+
390
+ it("validates without optional context", () => {
391
+ const event = {
392
+ type: "decomposition_generated",
393
+ project_key: "/test/project",
394
+ timestamp: Date.now(),
395
+ epic_id: "bd-123",
396
+ task: "Add user authentication",
397
+ strategy: "file-based",
398
+ epic_title: "User Authentication",
399
+ subtasks: [{ title: "Create OAuth flow", files: ["src/auth/oauth.ts"] }],
400
+ };
401
+ expect(() => DecompositionGeneratedEventSchema.parse(event)).not.toThrow();
402
+ });
403
+
404
+ it("validates strategy enum values", () => {
405
+ const validStrategies = ["file-based", "feature-based", "risk-based"];
406
+ for (const strategy of validStrategies) {
407
+ const event = {
408
+ type: "decomposition_generated",
409
+ project_key: "/test/project",
410
+ timestamp: Date.now(),
411
+ epic_id: "bd-123",
412
+ task: "Test task",
413
+ strategy,
414
+ epic_title: "Test",
415
+ subtasks: [{ title: "Subtask", files: ["test.ts"] }],
416
+ };
417
+ expect(() =>
418
+ DecompositionGeneratedEventSchema.parse(event),
419
+ ).not.toThrow();
420
+ }
421
+ });
422
+
423
+ it("rejects invalid strategy value", () => {
424
+ const event = {
425
+ type: "decomposition_generated",
426
+ project_key: "/test/project",
427
+ timestamp: Date.now(),
428
+ epic_id: "bd-123",
429
+ task: "Test task",
430
+ strategy: "invalid-strategy",
431
+ epic_title: "Test",
432
+ subtasks: [{ title: "Subtask", files: ["test.ts"] }],
433
+ };
434
+ expect(() => DecompositionGeneratedEventSchema.parse(event)).toThrow();
435
+ });
436
+
437
+ it("validates subtask priority bounds", () => {
438
+ const baseEvent = {
439
+ type: "decomposition_generated",
440
+ project_key: "/test/project",
441
+ timestamp: Date.now(),
442
+ epic_id: "bd-123",
443
+ task: "Test",
444
+ strategy: "file-based",
445
+ epic_title: "Test",
446
+ };
447
+
448
+ // Valid: 0
449
+ expect(() =>
450
+ DecompositionGeneratedEventSchema.parse({
451
+ ...baseEvent,
452
+ subtasks: [{ title: "Test", files: ["test.ts"], priority: 0 }],
453
+ }),
454
+ ).not.toThrow();
455
+
456
+ // Valid: 3
457
+ expect(() =>
458
+ DecompositionGeneratedEventSchema.parse({
459
+ ...baseEvent,
460
+ subtasks: [{ title: "Test", files: ["test.ts"], priority: 3 }],
461
+ }),
462
+ ).not.toThrow();
463
+
464
+ // Invalid: -1
465
+ expect(() =>
466
+ DecompositionGeneratedEventSchema.parse({
467
+ ...baseEvent,
468
+ subtasks: [{ title: "Test", files: ["test.ts"], priority: -1 }],
469
+ }),
470
+ ).toThrow();
471
+
472
+ // Invalid: 4
473
+ expect(() =>
474
+ DecompositionGeneratedEventSchema.parse({
475
+ ...baseEvent,
476
+ subtasks: [{ title: "Test", files: ["test.ts"], priority: 4 }],
477
+ }),
478
+ ).toThrow();
479
+ });
480
+
481
+ it("rejects empty subtasks array", () => {
482
+ const event = {
483
+ type: "decomposition_generated",
484
+ project_key: "/test/project",
485
+ timestamp: Date.now(),
486
+ epic_id: "bd-123",
487
+ task: "Test",
488
+ strategy: "file-based",
489
+ epic_title: "Test",
490
+ subtasks: [],
491
+ };
492
+ // Empty subtasks is valid per schema but semantically questionable
493
+ expect(() => DecompositionGeneratedEventSchema.parse(event)).not.toThrow();
494
+ });
495
+ });
496
+
497
+ describe("SubtaskOutcomeEventSchema", () => {
498
+ it("validates a complete subtask_outcome event", () => {
499
+ const event = {
500
+ type: "subtask_outcome",
501
+ project_key: "/test/project",
502
+ timestamp: Date.now(),
503
+ epic_id: "bd-123",
504
+ bead_id: "bd-123.1",
505
+ planned_files: ["src/auth.ts", "src/config.ts"],
506
+ actual_files: ["src/auth.ts", "src/config.ts", "src/utils.ts"],
507
+ duration_ms: 45000,
508
+ error_count: 2,
509
+ retry_count: 1,
510
+ success: true,
511
+ };
512
+ expect(() => SubtaskOutcomeEventSchema.parse(event)).not.toThrow();
513
+ });
514
+
515
+ it("applies defaults for error_count and retry_count", () => {
516
+ const event = {
517
+ type: "subtask_outcome",
518
+ project_key: "/test/project",
519
+ timestamp: Date.now(),
520
+ epic_id: "bd-123",
521
+ bead_id: "bd-123.1",
522
+ planned_files: ["src/auth.ts"],
523
+ actual_files: ["src/auth.ts"],
524
+ duration_ms: 10000,
525
+ success: true,
526
+ };
527
+ const parsed = SubtaskOutcomeEventSchema.parse(event);
528
+ expect(parsed.error_count).toBe(0);
529
+ expect(parsed.retry_count).toBe(0);
530
+ });
531
+
532
+ it("validates duration_ms is non-negative", () => {
533
+ const baseEvent = {
534
+ type: "subtask_outcome",
535
+ project_key: "/test/project",
536
+ timestamp: Date.now(),
537
+ epic_id: "bd-123",
538
+ bead_id: "bd-123.1",
539
+ planned_files: ["test.ts"],
540
+ actual_files: ["test.ts"],
541
+ success: true,
542
+ };
543
+
544
+ // Valid: 0
545
+ expect(() =>
546
+ SubtaskOutcomeEventSchema.parse({ ...baseEvent, duration_ms: 0 }),
547
+ ).not.toThrow();
548
+
549
+ // Valid: positive
550
+ expect(() =>
551
+ SubtaskOutcomeEventSchema.parse({ ...baseEvent, duration_ms: 1000 }),
552
+ ).not.toThrow();
553
+
554
+ // Invalid: negative
555
+ expect(() =>
556
+ SubtaskOutcomeEventSchema.parse({ ...baseEvent, duration_ms: -1 }),
557
+ ).toThrow();
558
+ });
559
+
560
+ it("validates error_count is non-negative", () => {
561
+ const baseEvent = {
562
+ type: "subtask_outcome",
563
+ project_key: "/test/project",
564
+ timestamp: Date.now(),
565
+ epic_id: "bd-123",
566
+ bead_id: "bd-123.1",
567
+ planned_files: ["test.ts"],
568
+ actual_files: ["test.ts"],
569
+ duration_ms: 1000,
570
+ success: true,
571
+ };
572
+
573
+ // Invalid: negative
574
+ expect(() =>
575
+ SubtaskOutcomeEventSchema.parse({ ...baseEvent, error_count: -1 }),
576
+ ).toThrow();
577
+ });
578
+
579
+ it("handles file lists with different lengths", () => {
580
+ const event = {
581
+ type: "subtask_outcome",
582
+ project_key: "/test/project",
583
+ timestamp: Date.now(),
584
+ epic_id: "bd-123",
585
+ bead_id: "bd-123.1",
586
+ planned_files: ["a.ts", "b.ts"],
587
+ actual_files: ["a.ts", "b.ts", "c.ts", "d.ts"],
588
+ duration_ms: 5000,
589
+ success: true,
590
+ };
591
+ expect(() => SubtaskOutcomeEventSchema.parse(event)).not.toThrow();
592
+ });
593
+ });
594
+
595
+ describe("HumanFeedbackEventSchema", () => {
596
+ it("validates a complete human_feedback event", () => {
597
+ const event = {
598
+ type: "human_feedback",
599
+ project_key: "/test/project",
600
+ timestamp: Date.now(),
601
+ epic_id: "bd-123",
602
+ accepted: true,
603
+ modified: false,
604
+ notes: "Looks good, no changes needed",
605
+ };
606
+ expect(() => HumanFeedbackEventSchema.parse(event)).not.toThrow();
607
+ });
608
+
609
+ it("validates accepted with modification", () => {
610
+ const event = {
611
+ type: "human_feedback",
612
+ project_key: "/test/project",
613
+ timestamp: Date.now(),
614
+ epic_id: "bd-123",
615
+ accepted: true,
616
+ modified: true,
617
+ notes: "Changed priority on subtask 2",
618
+ };
619
+ expect(() => HumanFeedbackEventSchema.parse(event)).not.toThrow();
620
+ });
621
+
622
+ it("validates rejected feedback", () => {
623
+ const event = {
624
+ type: "human_feedback",
625
+ project_key: "/test/project",
626
+ timestamp: Date.now(),
627
+ epic_id: "bd-123",
628
+ accepted: false,
629
+ modified: false,
630
+ notes: "Decomposition too granular, needs consolidation",
631
+ };
632
+ expect(() => HumanFeedbackEventSchema.parse(event)).not.toThrow();
633
+ });
634
+
635
+ it("applies default for modified", () => {
636
+ const event = {
637
+ type: "human_feedback",
638
+ project_key: "/test/project",
639
+ timestamp: Date.now(),
640
+ epic_id: "bd-123",
641
+ accepted: true,
642
+ };
643
+ const parsed = HumanFeedbackEventSchema.parse(event);
644
+ expect(parsed.modified).toBe(false);
645
+ });
646
+
647
+ it("validates without notes", () => {
648
+ const event = {
649
+ type: "human_feedback",
650
+ project_key: "/test/project",
651
+ timestamp: Date.now(),
652
+ epic_id: "bd-123",
653
+ accepted: true,
654
+ modified: false,
655
+ };
656
+ expect(() => HumanFeedbackEventSchema.parse(event)).not.toThrow();
657
+ });
658
+ });
659
+
364
660
  // ============================================================================
365
661
  // Discriminated Union Tests
366
662
  // ============================================================================
@@ -141,6 +141,85 @@ export const TaskBlockedEventSchema = BaseEventSchema.extend({
141
141
  reason: z.string(),
142
142
  });
143
143
 
144
+ // ============================================================================
145
+ // Eval Capture Events (for learning system)
146
+ // ============================================================================
147
+
148
+ export const DecompositionGeneratedEventSchema = BaseEventSchema.extend({
149
+ type: z.literal("decomposition_generated"),
150
+ epic_id: z.string(),
151
+ task: z.string(),
152
+ context: z.string().optional(),
153
+ strategy: z.enum(["file-based", "feature-based", "risk-based"]),
154
+ epic_title: z.string(),
155
+ subtasks: z.array(
156
+ z.object({
157
+ title: z.string(),
158
+ files: z.array(z.string()),
159
+ priority: z.number().min(0).max(3).optional(),
160
+ }),
161
+ ),
162
+ recovery_context: z
163
+ .object({
164
+ shared_context: z.string().optional(),
165
+ skills_to_load: z.array(z.string()).optional(),
166
+ coordinator_notes: z.string().optional(),
167
+ })
168
+ .optional(),
169
+ });
170
+
171
+ export const SubtaskOutcomeEventSchema = BaseEventSchema.extend({
172
+ type: z.literal("subtask_outcome"),
173
+ epic_id: z.string(),
174
+ bead_id: z.string(),
175
+ planned_files: z.array(z.string()),
176
+ actual_files: z.array(z.string()),
177
+ duration_ms: z.number().min(0),
178
+ error_count: z.number().min(0).default(0),
179
+ retry_count: z.number().min(0).default(0),
180
+ success: z.boolean(),
181
+ });
182
+
183
+ export const HumanFeedbackEventSchema = BaseEventSchema.extend({
184
+ type: z.literal("human_feedback"),
185
+ epic_id: z.string(),
186
+ accepted: z.boolean(),
187
+ modified: z.boolean().default(false),
188
+ notes: z.string().optional(),
189
+ });
190
+
191
+ // ============================================================================
192
+ // Swarm Checkpoint Events (for recovery and coordination)
193
+ // ============================================================================
194
+
195
+ export const SwarmCheckpointedEventSchema = BaseEventSchema.extend({
196
+ type: z.literal("swarm_checkpointed"),
197
+ epic_id: z.string(),
198
+ bead_id: z.string(),
199
+ strategy: z.enum(["file-based", "feature-based", "risk-based"]),
200
+ files: z.array(z.string()),
201
+ dependencies: z.array(z.string()),
202
+ directives: z.object({
203
+ shared_context: z.string().optional(),
204
+ skills_to_load: z.array(z.string()).optional(),
205
+ coordinator_notes: z.string().optional(),
206
+ }),
207
+ recovery: z.object({
208
+ last_checkpoint: z.number(),
209
+ files_modified: z.array(z.string()),
210
+ progress_percent: z.number().min(0).max(100),
211
+ last_message: z.string().optional(),
212
+ error_context: z.string().optional(),
213
+ }),
214
+ });
215
+
216
+ export const SwarmRecoveredEventSchema = BaseEventSchema.extend({
217
+ type: z.literal("swarm_recovered"),
218
+ epic_id: z.string(),
219
+ bead_id: z.string(),
220
+ recovered_from_checkpoint: z.number(), // timestamp
221
+ });
222
+
144
223
  // ============================================================================
145
224
  // Union Type
146
225
  // ============================================================================
@@ -157,6 +236,11 @@ export const AgentEventSchema = z.discriminatedUnion("type", [
157
236
  TaskProgressEventSchema,
158
237
  TaskCompletedEventSchema,
159
238
  TaskBlockedEventSchema,
239
+ DecompositionGeneratedEventSchema,
240
+ SubtaskOutcomeEventSchema,
241
+ HumanFeedbackEventSchema,
242
+ SwarmCheckpointedEventSchema,
243
+ SwarmRecoveredEventSchema,
160
244
  ]);
161
245
 
162
246
  export type AgentEvent = z.infer<typeof AgentEventSchema>;
@@ -173,6 +257,15 @@ export type TaskStartedEvent = z.infer<typeof TaskStartedEventSchema>;
173
257
  export type TaskProgressEvent = z.infer<typeof TaskProgressEventSchema>;
174
258
  export type TaskCompletedEvent = z.infer<typeof TaskCompletedEventSchema>;
175
259
  export type TaskBlockedEvent = z.infer<typeof TaskBlockedEventSchema>;
260
+ export type DecompositionGeneratedEvent = z.infer<
261
+ typeof DecompositionGeneratedEventSchema
262
+ >;
263
+ export type SubtaskOutcomeEvent = z.infer<typeof SubtaskOutcomeEventSchema>;
264
+ export type HumanFeedbackEvent = z.infer<typeof HumanFeedbackEventSchema>;
265
+ export type SwarmCheckpointedEvent = z.infer<
266
+ typeof SwarmCheckpointedEventSchema
267
+ >;
268
+ export type SwarmRecoveredEvent = z.infer<typeof SwarmRecoveredEventSchema>;
176
269
 
177
270
  // ============================================================================
178
271
  // Session State Types
@@ -34,11 +34,11 @@ describe("Schema Migrations", () => {
34
34
  it("should run all migrations on fresh database", async () => {
35
35
  const result = await runMigrations(db);
36
36
 
37
- expect(result.applied).toEqual([1, 2]);
38
- expect(result.current).toBe(2);
37
+ expect(result.applied).toEqual([1, 2, 3, 4]);
38
+ expect(result.current).toBe(4);
39
39
 
40
40
  const version = await getCurrentVersion(db);
41
- expect(version).toBe(2);
41
+ expect(version).toBe(4);
42
42
  });
43
43
 
44
44
  it("should create cursors table with correct schema", async () => {
@@ -105,16 +105,16 @@ describe("Schema Migrations", () => {
105
105
  it("should be safe to run migrations multiple times", async () => {
106
106
  // First run
107
107
  const result1 = await runMigrations(db);
108
- expect(result1.applied).toEqual([1, 2]);
108
+ expect(result1.applied).toEqual([1, 2, 3, 4]);
109
109
 
110
110
  // Second run - should apply nothing
111
111
  const result2 = await runMigrations(db);
112
112
  expect(result2.applied).toEqual([]);
113
- expect(result2.current).toBe(2);
113
+ expect(result2.current).toBe(4);
114
114
 
115
115
  // Version should still be 2
116
116
  const version = await getCurrentVersion(db);
117
- expect(version).toBe(2);
117
+ expect(version).toBe(4);
118
118
  });
119
119
  });
120
120
 
@@ -137,8 +137,8 @@ describe("Schema Migrations", () => {
137
137
 
138
138
  // Now run migrations - should only apply 2
139
139
  const result = await runMigrations(db);
140
- expect(result.applied).toEqual([2]);
141
- expect(result.current).toBe(2);
140
+ expect(result.applied).toEqual([2, 3, 4]);
141
+ expect(result.current).toBe(4);
142
142
  });
143
143
  });
144
144
 
@@ -146,11 +146,11 @@ describe("Schema Migrations", () => {
146
146
  it("should rollback to target version", async () => {
147
147
  // Apply all migrations
148
148
  await runMigrations(db);
149
- expect(await getCurrentVersion(db)).toBe(2);
149
+ expect(await getCurrentVersion(db)).toBe(4);
150
150
 
151
151
  // Rollback to version 1
152
152
  const result = await rollbackTo(db, 1);
153
- expect(result.rolledBack).toEqual([2]);
153
+ expect(result.rolledBack).toEqual([4, 3, 2]);
154
154
  expect(result.current).toBe(1);
155
155
 
156
156
  // Version should be 1
@@ -180,7 +180,7 @@ describe("Schema Migrations", () => {
180
180
  await runMigrations(db);
181
181
 
182
182
  const result = await rollbackTo(db, 0);
183
- expect(result.rolledBack).toEqual([2, 1]);
183
+ expect(result.rolledBack).toEqual([4, 3, 2, 1]);
184
184
  expect(result.current).toBe(0);
185
185
 
186
186
  // All tables should be gone
@@ -196,9 +196,9 @@ describe("Schema Migrations", () => {
196
196
  it("should do nothing if target version >= current", async () => {
197
197
  await runMigrations(db);
198
198
 
199
- const result = await rollbackTo(db, 2);
199
+ const result = await rollbackTo(db, 4);
200
200
  expect(result.rolledBack).toEqual([]);
201
- expect(result.current).toBe(2);
201
+ expect(result.current).toBe(4);
202
202
  });
203
203
  });
204
204
 
@@ -210,12 +210,16 @@ describe("Schema Migrations", () => {
210
210
 
211
211
  expect(await isMigrationApplied(db, 1)).toBe(true);
212
212
  expect(await isMigrationApplied(db, 2)).toBe(true);
213
+ expect(await isMigrationApplied(db, 3)).toBe(true);
214
+ expect(await isMigrationApplied(db, 4)).toBe(true);
215
+ expect(await isMigrationApplied(db, 3)).toBe(true);
216
+ expect(await isMigrationApplied(db, 4)).toBe(true);
213
217
  });
214
218
 
215
219
  it("should list pending migrations", async () => {
216
220
  const pending1 = await getPendingMigrations(db);
217
- expect(pending1).toHaveLength(2);
218
- expect(pending1.map((m) => m.version)).toEqual([1, 2]);
221
+ expect(pending1).toHaveLength(4);
222
+ expect(pending1.map((m) => m.version)).toEqual([1, 2, 3, 4]);
219
223
 
220
224
  // Apply migration 1
221
225
  const migration = migrations[0];
@@ -236,8 +240,8 @@ describe("Schema Migrations", () => {
236
240
  );
237
241
 
238
242
  const pending2 = await getPendingMigrations(db);
239
- expect(pending2).toHaveLength(1);
240
- expect(pending2.map((m) => m.version)).toEqual([2]);
243
+ expect(pending2).toHaveLength(3);
244
+ expect(pending2.map((m) => m.version)).toEqual([2, 3, 4]);
241
245
  });
242
246
 
243
247
  it("should list applied migrations", async () => {
@@ -247,8 +251,8 @@ describe("Schema Migrations", () => {
247
251
  await runMigrations(db);
248
252
 
249
253
  const applied2 = await getAppliedMigrations(db);
250
- expect(applied2).toHaveLength(2);
251
- expect(applied2.map((m) => m.version)).toEqual([1, 2]);
254
+ expect(applied2).toHaveLength(4);
255
+ expect(applied2.map((m) => m.version)).toEqual([1, 2, 3, 4]);
252
256
  expect(applied2[0]?.description).toBe(
253
257
  "Add cursors table for DurableCursor",
254
258
  );
@@ -340,7 +344,7 @@ describe("Schema Migrations", () => {
340
344
  `SELECT version, applied_at, description FROM schema_version ORDER BY version`,
341
345
  );
342
346
 
343
- expect(result.rows).toHaveLength(2);
347
+ expect(result.rows).toHaveLength(4);
344
348
  expect(result.rows[0]?.version).toBe(1);
345
349
  expect(result.rows[0]?.description).toBe(
346
350
  "Add cursors table for DurableCursor",
@@ -107,6 +107,57 @@ export const migrations: Migration[] = [
107
107
  `,
108
108
  down: `DROP TABLE IF EXISTS deferred;`,
109
109
  },
110
+ {
111
+ version: 3,
112
+ description: "Add eval_records table for learning system",
113
+ up: `
114
+ CREATE TABLE IF NOT EXISTS eval_records (
115
+ id TEXT PRIMARY KEY,
116
+ project_key TEXT NOT NULL,
117
+ task TEXT NOT NULL,
118
+ context TEXT,
119
+ strategy TEXT NOT NULL,
120
+ epic_title TEXT NOT NULL,
121
+ subtasks JSONB NOT NULL,
122
+ outcomes JSONB,
123
+ overall_success BOOLEAN,
124
+ total_duration_ms INTEGER,
125
+ total_errors INTEGER,
126
+ human_accepted BOOLEAN,
127
+ human_modified BOOLEAN,
128
+ human_notes TEXT,
129
+ file_overlap_count INTEGER,
130
+ scope_accuracy REAL,
131
+ time_balance_ratio REAL,
132
+ created_at BIGINT NOT NULL,
133
+ updated_at BIGINT NOT NULL
134
+ );
135
+ CREATE INDEX IF NOT EXISTS idx_eval_records_project ON eval_records(project_key);
136
+ CREATE INDEX IF NOT EXISTS idx_eval_records_strategy ON eval_records(strategy);
137
+ `,
138
+ down: `DROP TABLE IF EXISTS eval_records;`,
139
+ },
140
+ {
141
+ version: 4,
142
+ description: "Add swarm_contexts table for context recovery",
143
+ up: `
144
+ CREATE TABLE IF NOT EXISTS swarm_contexts (
145
+ id TEXT PRIMARY KEY,
146
+ epic_id TEXT NOT NULL,
147
+ bead_id TEXT NOT NULL,
148
+ strategy TEXT NOT NULL,
149
+ files JSONB NOT NULL,
150
+ dependencies JSONB NOT NULL,
151
+ directives JSONB NOT NULL,
152
+ recovery JSONB NOT NULL,
153
+ created_at BIGINT NOT NULL,
154
+ updated_at BIGINT NOT NULL
155
+ );
156
+ CREATE INDEX IF NOT EXISTS idx_swarm_contexts_epic ON swarm_contexts(epic_id);
157
+ CREATE INDEX IF NOT EXISTS idx_swarm_contexts_bead ON swarm_contexts(bead_id);
158
+ `,
159
+ down: `DROP TABLE IF EXISTS swarm_contexts;`,
160
+ },
110
161
  ];
111
162
 
112
163
  // ============================================================================