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.
- package/.beads/issues.jsonl +213 -0
- package/INTEGRATION_EXAMPLE.md +66 -0
- package/README.md +352 -522
- package/dist/index.js +2046 -984
- package/dist/plugin.js +2051 -1017
- package/docs/analysis/subagent-coordination-patterns.md +2 -0
- package/docs/semantic-memory-cli-syntax.md +123 -0
- package/docs/swarm-mail-architecture.md +1147 -0
- package/evals/README.md +116 -0
- package/evals/evalite.config.ts +15 -0
- package/evals/example.eval.ts +32 -0
- package/evals/fixtures/decomposition-cases.ts +105 -0
- package/evals/lib/data-loader.test.ts +288 -0
- package/evals/lib/data-loader.ts +111 -0
- package/evals/lib/llm.ts +115 -0
- package/evals/scorers/index.ts +200 -0
- package/evals/scorers/outcome-scorers.test.ts +27 -0
- package/evals/scorers/outcome-scorers.ts +349 -0
- package/evals/swarm-decomposition.eval.ts +112 -0
- package/package.json +8 -1
- package/scripts/cleanup-test-memories.ts +346 -0
- package/src/beads.ts +49 -0
- package/src/eval-capture.ts +487 -0
- package/src/index.ts +45 -3
- package/src/learning.integration.test.ts +19 -4
- package/src/output-guardrails.test.ts +438 -0
- package/src/output-guardrails.ts +381 -0
- package/src/schemas/index.ts +18 -0
- package/src/schemas/swarm-context.ts +115 -0
- package/src/storage.ts +117 -5
- package/src/streams/events.test.ts +296 -0
- package/src/streams/events.ts +93 -0
- package/src/streams/migrations.test.ts +24 -20
- package/src/streams/migrations.ts +51 -0
- package/src/streams/projections.ts +187 -0
- package/src/streams/store.ts +275 -0
- package/src/swarm-orchestrate.ts +771 -189
- package/src/swarm-prompts.ts +84 -12
- package/src/swarm.integration.test.ts +124 -0
- package/vitest.integration.config.ts +6 -0
- 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
|
// ============================================================================
|
package/src/streams/events.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
199
|
+
const result = await rollbackTo(db, 4);
|
|
200
200
|
expect(result.rolledBack).toEqual([]);
|
|
201
|
-
expect(result.current).toBe(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
// ============================================================================
|