opencode-swarm-plugin 0.19.0 → 0.21.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.
@@ -125,3 +125,21 @@ export {
125
125
  type QueryMandatesArgs,
126
126
  type ScoreCalculationResult,
127
127
  } from "./mandate";
128
+
129
+ // Swarm context schemas
130
+ export {
131
+ SwarmStrategySchema,
132
+ SwarmDirectivesSchema,
133
+ SwarmRecoverySchema,
134
+ SwarmBeadContextSchema,
135
+ CreateSwarmContextArgsSchema,
136
+ UpdateSwarmContextArgsSchema,
137
+ QuerySwarmContextsArgsSchema,
138
+ type SwarmStrategy,
139
+ type SwarmDirectives,
140
+ type SwarmRecovery,
141
+ type SwarmBeadContext,
142
+ type CreateSwarmContextArgs,
143
+ type UpdateSwarmContextArgs,
144
+ type QuerySwarmContextsArgs,
145
+ } from "./swarm-context";
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Swarm Context Schemas
3
+ *
4
+ * These schemas define the structure for storing and recovering swarm execution context.
5
+ * Used for checkpoint/recovery, continuation after crashes, and swarm state management.
6
+ */
7
+ import { z } from "zod";
8
+
9
+ /**
10
+ * Decomposition strategy used for the swarm
11
+ */
12
+ export const SwarmStrategySchema = z.enum([
13
+ "file-based",
14
+ "feature-based",
15
+ "risk-based",
16
+ ]);
17
+ export type SwarmStrategy = z.infer<typeof SwarmStrategySchema>;
18
+
19
+ /**
20
+ * Shared directives and context for all agents in a swarm
21
+ */
22
+ export const SwarmDirectivesSchema = z.object({
23
+ /** Context shared with all agents (API contracts, conventions, arch decisions) */
24
+ shared_context: z.string(),
25
+ /** Skills to load in agent context (e.g., ['testing-patterns', 'swarm-coordination']) */
26
+ skills_to_load: z.array(z.string()).default([]),
27
+ /** Notes from coordinator to agents (gotchas, important context) */
28
+ coordinator_notes: z.string().default(""),
29
+ });
30
+ export type SwarmDirectives = z.infer<typeof SwarmDirectivesSchema>;
31
+
32
+ /**
33
+ * Recovery state for checkpoint/resume
34
+ */
35
+ export const SwarmRecoverySchema = z.object({
36
+ /** Last known checkpoint (ISO-8601 timestamp or checkpoint ID) */
37
+ last_checkpoint: z.string(),
38
+ /** Files modified since checkpoint (for rollback/recovery) */
39
+ files_modified: z.array(z.string()).default([]),
40
+ /** Progress percentage (0-100) */
41
+ progress_percent: z.number().min(0).max(100).default(0),
42
+ /** Last status message from agent */
43
+ last_message: z.string().default(""),
44
+ /** Error context if agent failed (for retry/recovery) */
45
+ error_context: z.string().optional(),
46
+ });
47
+ export type SwarmRecovery = z.infer<typeof SwarmRecoverySchema>;
48
+
49
+ /**
50
+ * Complete context for a single bead in a swarm
51
+ *
52
+ * Stored in swarm_contexts table for recovery, continuation, and state management.
53
+ */
54
+ export const SwarmBeadContextSchema = z.object({
55
+ /** ID of the swarm context record */
56
+ id: z.string(),
57
+ /** Epic this bead belongs to */
58
+ epic_id: z.string(),
59
+ /** Bead ID being executed */
60
+ bead_id: z.string(),
61
+ /** Decomposition strategy used */
62
+ strategy: SwarmStrategySchema,
63
+ /** Files this bead is responsible for */
64
+ files: z.array(z.string()),
65
+ /** Bead IDs this task depends on */
66
+ dependencies: z.array(z.string()).default([]),
67
+ /** Shared directives and context */
68
+ directives: SwarmDirectivesSchema,
69
+ /** Recovery state */
70
+ recovery: SwarmRecoverySchema,
71
+ /** Creation timestamp (epoch ms) */
72
+ created_at: z.number().int().positive(),
73
+ /** Last update timestamp (epoch ms) */
74
+ updated_at: z.number().int().positive(),
75
+ });
76
+ export type SwarmBeadContext = z.infer<typeof SwarmBeadContextSchema>;
77
+
78
+ /**
79
+ * Args for creating a swarm context
80
+ */
81
+ export const CreateSwarmContextArgsSchema = SwarmBeadContextSchema.omit({
82
+ id: true,
83
+ created_at: true,
84
+ updated_at: true,
85
+ });
86
+ export type CreateSwarmContextArgs = z.infer<
87
+ typeof CreateSwarmContextArgsSchema
88
+ >;
89
+
90
+ /**
91
+ * Args for updating a swarm context
92
+ */
93
+ export const UpdateSwarmContextArgsSchema = z.object({
94
+ id: z.string(),
95
+ recovery: SwarmRecoverySchema.partial().optional(),
96
+ files: z.array(z.string()).optional(),
97
+ dependencies: z.array(z.string()).optional(),
98
+ directives: SwarmDirectivesSchema.partial().optional(),
99
+ });
100
+ export type UpdateSwarmContextArgs = z.infer<
101
+ typeof UpdateSwarmContextArgsSchema
102
+ >;
103
+
104
+ /**
105
+ * Args for querying swarm contexts
106
+ */
107
+ export const QuerySwarmContextsArgsSchema = z.object({
108
+ epic_id: z.string().optional(),
109
+ bead_id: z.string().optional(),
110
+ strategy: SwarmStrategySchema.optional(),
111
+ has_errors: z.boolean().optional(), // Filter by presence of error_context
112
+ });
113
+ export type QuerySwarmContextsArgs = z.infer<
114
+ typeof QuerySwarmContextsArgsSchema
115
+ >;
@@ -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,37 @@ 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>;
269
+
270
+ // ============================================================================
271
+ // Session State Types
272
+ // ============================================================================
273
+
274
+ /**
275
+ * Shared session state for Agent Mail and Swarm Mail
276
+ *
277
+ * Common fields for tracking agent coordination session across both
278
+ * the MCP-based implementation (agent-mail) and the embedded event-sourced
279
+ * implementation (swarm-mail).
280
+ */
281
+ export interface MailSessionState {
282
+ /** Project key (usually absolute path) */
283
+ projectKey: string;
284
+ /** Agent name for this session */
285
+ agentName: string;
286
+ /** Active reservation IDs */
287
+ reservations: number[];
288
+ /** Session start timestamp (ISO-8601) */
289
+ startedAt: string;
290
+ }
176
291
 
177
292
  // ============================================================================
178
293
  // Event Helpers