opencode-swarm-plugin 0.12.31 → 0.13.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 (49) hide show
  1. package/.beads/issues.jsonl +204 -10
  2. package/.opencode/skills/tdd/SKILL.md +182 -0
  3. package/README.md +165 -17
  4. package/bun.lock +23 -0
  5. package/dist/index.js +4082 -457
  6. package/dist/pglite.data +0 -0
  7. package/dist/pglite.wasm +0 -0
  8. package/dist/plugin.js +4070 -533
  9. package/examples/commands/swarm.md +100 -28
  10. package/examples/skills/beads-workflow/SKILL.md +75 -28
  11. package/examples/skills/swarm-coordination/SKILL.md +165 -21
  12. package/global-skills/swarm-coordination/SKILL.md +116 -58
  13. package/global-skills/testing-patterns/SKILL.md +430 -0
  14. package/global-skills/testing-patterns/references/dependency-breaking-catalog.md +586 -0
  15. package/package.json +11 -5
  16. package/src/index.ts +44 -5
  17. package/src/streams/agent-mail.test.ts +777 -0
  18. package/src/streams/agent-mail.ts +535 -0
  19. package/src/streams/debug.test.ts +500 -0
  20. package/src/streams/debug.ts +629 -0
  21. package/src/streams/effect/ask.integration.test.ts +314 -0
  22. package/src/streams/effect/ask.ts +202 -0
  23. package/src/streams/effect/cursor.integration.test.ts +418 -0
  24. package/src/streams/effect/cursor.ts +288 -0
  25. package/src/streams/effect/deferred.test.ts +357 -0
  26. package/src/streams/effect/deferred.ts +445 -0
  27. package/src/streams/effect/index.ts +17 -0
  28. package/src/streams/effect/layers.ts +73 -0
  29. package/src/streams/effect/lock.test.ts +385 -0
  30. package/src/streams/effect/lock.ts +399 -0
  31. package/src/streams/effect/mailbox.test.ts +260 -0
  32. package/src/streams/effect/mailbox.ts +318 -0
  33. package/src/streams/events.test.ts +628 -0
  34. package/src/streams/events.ts +214 -0
  35. package/src/streams/index.test.ts +229 -0
  36. package/src/streams/index.ts +492 -0
  37. package/src/streams/migrations.test.ts +355 -0
  38. package/src/streams/migrations.ts +269 -0
  39. package/src/streams/projections.test.ts +611 -0
  40. package/src/streams/projections.ts +302 -0
  41. package/src/streams/store.integration.test.ts +548 -0
  42. package/src/streams/store.ts +546 -0
  43. package/src/streams/swarm-mail.ts +552 -0
  44. package/src/swarm-mail.integration.test.ts +970 -0
  45. package/src/swarm-mail.ts +739 -0
  46. package/src/swarm.integration.test.ts +16 -10
  47. package/src/swarm.ts +146 -78
  48. package/src/tool-availability.ts +35 -2
  49. package/global-skills/mcp-tool-authoring/SKILL.md +0 -695
@@ -0,0 +1,629 @@
1
+ /**
2
+ * Swarm Mail Debug Tools - Event inspection and state debugging
3
+ *
4
+ * Tools for inspecting the event store, agent state, and system health.
5
+ * Useful for debugging issues and understanding system behavior.
6
+ */
7
+ import { getDatabase, getDatabaseStats } from "./index";
8
+ import { readEvents, getLatestSequence } from "./store";
9
+ import { getAgent, getActiveReservations, getMessage } from "./projections";
10
+ import type { AgentEvent } from "./events";
11
+
12
+ // ============================================================================
13
+ // Types
14
+ // ============================================================================
15
+
16
+ export interface DebugEventsOptions {
17
+ projectPath: string;
18
+ types?: AgentEvent["type"][];
19
+ agentName?: string;
20
+ limit?: number;
21
+ since?: number;
22
+ until?: number;
23
+ }
24
+
25
+ export interface DebugEventResult {
26
+ id: number;
27
+ sequence: number;
28
+ type: AgentEvent["type"];
29
+ timestamp: number;
30
+ timestamp_human: string;
31
+ agent_name?: string;
32
+ from_agent?: string;
33
+ to_agents?: string[];
34
+ [key: string]: unknown;
35
+ }
36
+
37
+ export interface DebugEventsResult {
38
+ events: DebugEventResult[];
39
+ total: number;
40
+ }
41
+
42
+ export interface DebugAgentOptions {
43
+ projectPath: string;
44
+ agentName: string;
45
+ includeEvents?: boolean;
46
+ }
47
+
48
+ export interface DebugAgentResult {
49
+ agent: {
50
+ name: string;
51
+ program: string;
52
+ model: string;
53
+ task_description: string | null;
54
+ registered_at: number;
55
+ last_active_at: number;
56
+ } | null;
57
+ stats: {
58
+ messagesSent: number;
59
+ messagesReceived: number;
60
+ };
61
+ reservations: Array<{
62
+ id: number;
63
+ path: string;
64
+ reason: string | null;
65
+ expires_at: number;
66
+ }>;
67
+ recentEvents?: DebugEventResult[];
68
+ }
69
+
70
+ export interface DebugMessageOptions {
71
+ projectPath: string;
72
+ messageId: number;
73
+ includeEvents?: boolean;
74
+ }
75
+
76
+ export interface DebugMessageResult {
77
+ message: {
78
+ id: number;
79
+ from_agent: string;
80
+ subject: string;
81
+ body: string;
82
+ thread_id: string | null;
83
+ importance: string;
84
+ created_at: number;
85
+ } | null;
86
+ recipients: Array<{
87
+ agent_name: string;
88
+ read_at: number | null;
89
+ acked_at: number | null;
90
+ }>;
91
+ events?: DebugEventResult[];
92
+ }
93
+
94
+ export interface DebugReservationsOptions {
95
+ projectPath: string;
96
+ checkConflicts?: boolean;
97
+ }
98
+
99
+ export interface DebugReservationsResult {
100
+ reservations: Array<{
101
+ id: number;
102
+ agent_name: string;
103
+ path_pattern: string;
104
+ reason: string | null;
105
+ expires_at: number;
106
+ expires_in_human: string;
107
+ }>;
108
+ byAgent: Record<string, Array<{ path: string; expires_at: number }>>;
109
+ conflicts?: Array<{
110
+ path1: string;
111
+ agent1: string;
112
+ path2: string;
113
+ agent2: string;
114
+ }>;
115
+ }
116
+
117
+ export interface TimelineEntry {
118
+ time: string;
119
+ type: AgentEvent["type"];
120
+ summary: string;
121
+ agent: string;
122
+ sequence: number;
123
+ }
124
+
125
+ export interface TimelineResult {
126
+ timeline: TimelineEntry[];
127
+ }
128
+
129
+ export interface InspectStateOptions {
130
+ projectPath: string;
131
+ format?: "object" | "json";
132
+ }
133
+
134
+ export interface InspectStateResult {
135
+ agents: Array<{
136
+ name: string;
137
+ program: string;
138
+ model: string;
139
+ task_description: string | null;
140
+ }>;
141
+ messages: Array<{
142
+ id: number;
143
+ from_agent: string;
144
+ subject: string;
145
+ thread_id: string | null;
146
+ }>;
147
+ reservations: Array<{
148
+ id: number;
149
+ agent_name: string;
150
+ path_pattern: string;
151
+ }>;
152
+ eventCount: number;
153
+ latestSequence: number;
154
+ stats: {
155
+ events: number;
156
+ agents: number;
157
+ messages: number;
158
+ reservations: number;
159
+ };
160
+ json?: string;
161
+ }
162
+
163
+ // ============================================================================
164
+ // Helper Functions
165
+ // ============================================================================
166
+
167
+ /**
168
+ * Format timestamp as human-readable ISO string
169
+ */
170
+ function formatTimestamp(timestamp: number): string {
171
+ return new Date(timestamp).toISOString();
172
+ }
173
+
174
+ /**
175
+ * Format duration as human-readable string
176
+ */
177
+ function formatDuration(ms: number): string {
178
+ if (ms < 0) return "expired";
179
+ if (ms < 60000) return `${Math.round(ms / 1000)}s`;
180
+ if (ms < 3600000) return `${Math.round(ms / 60000)}m`;
181
+ if (ms < 86400000) return `${Math.round(ms / 3600000)}h`;
182
+ return `${Math.round(ms / 86400000)}d`;
183
+ }
184
+
185
+ /**
186
+ * Generate event summary for timeline
187
+ */
188
+ function summarizeEvent(
189
+ event: AgentEvent & { id: number; sequence: number },
190
+ ): string {
191
+ switch (event.type) {
192
+ case "agent_registered":
193
+ return `Agent ${event.agent_name} registered (${event.program}/${event.model})`;
194
+ case "agent_active":
195
+ return `Agent ${event.agent_name} active`;
196
+ case "message_sent":
197
+ return `${event.from_agent} → ${event.to_agents.join(", ")}: "${event.subject}"`;
198
+ case "message_read":
199
+ return `${event.agent_name} read message #${event.message_id}`;
200
+ case "message_acked":
201
+ return `${event.agent_name} acked message #${event.message_id}`;
202
+ case "file_reserved":
203
+ return `${event.agent_name} reserved ${event.paths.length} file(s)`;
204
+ case "file_released":
205
+ return `${event.agent_name} released files`;
206
+ case "task_started":
207
+ return `${event.agent_name} started task: ${event.bead_id}`;
208
+ case "task_progress":
209
+ return `${event.agent_name} progress on ${event.bead_id}: ${event.progress_percent}%`;
210
+ case "task_completed":
211
+ return `${event.agent_name} completed ${event.bead_id}`;
212
+ case "task_blocked":
213
+ return `${event.agent_name} blocked on ${event.bead_id}: ${event.reason}`;
214
+ default:
215
+ return `Unknown event type`;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Get agent name from event (handles different event types)
221
+ */
222
+ function getAgentFromEvent(event: AgentEvent): string {
223
+ if ("agent_name" in event && event.agent_name) return event.agent_name;
224
+ if ("from_agent" in event && event.from_agent) return event.from_agent;
225
+ return "unknown";
226
+ }
227
+
228
+ // ============================================================================
229
+ // Debug Functions
230
+ // ============================================================================
231
+
232
+ /**
233
+ * Get recent events with filtering
234
+ */
235
+ export async function debugEvents(
236
+ options: DebugEventsOptions,
237
+ ): Promise<DebugEventsResult> {
238
+ const { projectPath, types, agentName, limit = 50, since, until } = options;
239
+
240
+ // Get all events first (we'll filter in memory for agent name)
241
+ const allEvents = await readEvents(
242
+ {
243
+ projectKey: projectPath,
244
+ types,
245
+ since,
246
+ until,
247
+ },
248
+ projectPath,
249
+ );
250
+
251
+ // Filter by agent name if specified
252
+ let filteredEvents = allEvents;
253
+ if (agentName) {
254
+ filteredEvents = allEvents.filter((e) => {
255
+ if ("agent_name" in e && e.agent_name === agentName) return true;
256
+ if ("from_agent" in e && e.from_agent === agentName) return true;
257
+ if ("to_agents" in e && e.to_agents?.includes(agentName)) return true;
258
+ return false;
259
+ });
260
+ }
261
+
262
+ // Sort by sequence descending (most recent first)
263
+ filteredEvents.sort((a, b) => b.sequence - a.sequence);
264
+
265
+ // Apply limit
266
+ const limitedEvents = filteredEvents.slice(0, limit);
267
+
268
+ // Format for output - extract known fields, spread rest
269
+ const events: DebugEventResult[] = limitedEvents.map((e) => {
270
+ const { id, sequence, type, timestamp, project_key, ...rest } = e;
271
+ return {
272
+ id,
273
+ sequence,
274
+ type,
275
+ timestamp,
276
+ timestamp_human: formatTimestamp(timestamp),
277
+ ...rest,
278
+ };
279
+ });
280
+
281
+ return {
282
+ events,
283
+ total: filteredEvents.length,
284
+ };
285
+ }
286
+
287
+ /**
288
+ * Get detailed agent information
289
+ */
290
+ export async function debugAgent(
291
+ options: DebugAgentOptions,
292
+ ): Promise<DebugAgentResult> {
293
+ const { projectPath, agentName, includeEvents = false } = options;
294
+
295
+ // Get agent from projections
296
+ const agent = await getAgent(projectPath, agentName, projectPath);
297
+
298
+ if (!agent) {
299
+ return {
300
+ agent: null,
301
+ stats: { messagesSent: 0, messagesReceived: 0 },
302
+ reservations: [],
303
+ };
304
+ }
305
+
306
+ // Get message counts
307
+ const db = await getDatabase(projectPath);
308
+ const sentResult = await db.query<{ count: string }>(
309
+ `SELECT COUNT(*) as count FROM messages WHERE project_key = $1 AND from_agent = $2`,
310
+ [projectPath, agentName],
311
+ );
312
+ const receivedResult = await db.query<{ count: string }>(
313
+ `SELECT COUNT(*) as count FROM message_recipients mr
314
+ JOIN messages m ON mr.message_id = m.id
315
+ WHERE m.project_key = $1 AND mr.agent_name = $2`,
316
+ [projectPath, agentName],
317
+ );
318
+
319
+ // Get active reservations
320
+ const reservations = await getActiveReservations(
321
+ projectPath,
322
+ projectPath,
323
+ agentName,
324
+ );
325
+
326
+ const result: DebugAgentResult = {
327
+ agent: {
328
+ name: agent.name,
329
+ program: agent.program,
330
+ model: agent.model,
331
+ task_description: agent.task_description,
332
+ registered_at: agent.registered_at,
333
+ last_active_at: agent.last_active_at,
334
+ },
335
+ stats: {
336
+ messagesSent: parseInt(sentResult.rows[0]?.count || "0"),
337
+ messagesReceived: parseInt(receivedResult.rows[0]?.count || "0"),
338
+ },
339
+ reservations: reservations.map((r) => ({
340
+ id: r.id,
341
+ path: r.path_pattern,
342
+ reason: r.reason,
343
+ expires_at: r.expires_at,
344
+ })),
345
+ };
346
+
347
+ // Include recent events if requested
348
+ if (includeEvents) {
349
+ const eventsResult = await debugEvents({
350
+ projectPath,
351
+ agentName,
352
+ limit: 20,
353
+ });
354
+ result.recentEvents = eventsResult.events;
355
+ }
356
+
357
+ return result;
358
+ }
359
+
360
+ /**
361
+ * Get detailed message information with audit trail
362
+ */
363
+ export async function debugMessage(
364
+ options: DebugMessageOptions,
365
+ ): Promise<DebugMessageResult> {
366
+ const { projectPath, messageId, includeEvents = false } = options;
367
+
368
+ // Get message from projections
369
+ const message = await getMessage(projectPath, messageId, projectPath);
370
+
371
+ if (!message) {
372
+ return {
373
+ message: null,
374
+ recipients: [],
375
+ };
376
+ }
377
+
378
+ // Get recipients
379
+ const db = await getDatabase(projectPath);
380
+ const recipientsResult = await db.query<{
381
+ agent_name: string;
382
+ read_at: string | null;
383
+ acked_at: string | null;
384
+ }>(
385
+ `SELECT agent_name, read_at, acked_at FROM message_recipients WHERE message_id = $1`,
386
+ [messageId],
387
+ );
388
+
389
+ const result: DebugMessageResult = {
390
+ message: {
391
+ id: message.id,
392
+ from_agent: message.from_agent,
393
+ subject: message.subject,
394
+ body: message.body ?? "",
395
+ thread_id: message.thread_id,
396
+ importance: message.importance,
397
+ created_at: message.created_at,
398
+ },
399
+ recipients: recipientsResult.rows.map((r) => ({
400
+ agent_name: r.agent_name,
401
+ read_at: r.read_at ? parseInt(r.read_at) : null,
402
+ acked_at: r.acked_at ? parseInt(r.acked_at) : null,
403
+ })),
404
+ };
405
+
406
+ // Include related events if requested
407
+ if (includeEvents) {
408
+ const allEvents = await readEvents(
409
+ { projectKey: projectPath },
410
+ projectPath,
411
+ );
412
+ const relatedEvents = allEvents.filter((e) => {
413
+ if (e.type === "message_sent" && e.subject === message.subject)
414
+ return true;
415
+ if (
416
+ (e.type === "message_read" || e.type === "message_acked") &&
417
+ e.message_id === messageId
418
+ )
419
+ return true;
420
+ return false;
421
+ });
422
+
423
+ result.events = relatedEvents.map((e) => {
424
+ const { id, sequence, type, timestamp, project_key, ...rest } = e;
425
+ return {
426
+ id,
427
+ sequence,
428
+ type,
429
+ timestamp,
430
+ timestamp_human: formatTimestamp(timestamp),
431
+ ...rest,
432
+ };
433
+ });
434
+ }
435
+
436
+ return result;
437
+ }
438
+
439
+ /**
440
+ * Get current reservation state
441
+ */
442
+ export async function debugReservations(
443
+ options: DebugReservationsOptions,
444
+ ): Promise<DebugReservationsResult> {
445
+ const { projectPath, checkConflicts = false } = options;
446
+
447
+ const reservations = await getActiveReservations(projectPath, projectPath);
448
+ const now = Date.now();
449
+
450
+ // Format reservations
451
+ const formattedReservations = reservations.map((r) => ({
452
+ id: r.id,
453
+ agent_name: r.agent_name,
454
+ path_pattern: r.path_pattern,
455
+ reason: r.reason,
456
+ expires_at: r.expires_at,
457
+ expires_in_human: formatDuration(r.expires_at - now),
458
+ }));
459
+
460
+ // Group by agent
461
+ const byAgent: Record<
462
+ string,
463
+ Array<{ path: string; expires_at: number }>
464
+ > = {};
465
+ for (const r of reservations) {
466
+ if (!byAgent[r.agent_name]) {
467
+ byAgent[r.agent_name] = [];
468
+ }
469
+ byAgent[r.agent_name].push({
470
+ path: r.path_pattern,
471
+ expires_at: r.expires_at,
472
+ });
473
+ }
474
+
475
+ const result: DebugReservationsResult = {
476
+ reservations: formattedReservations,
477
+ byAgent,
478
+ };
479
+
480
+ // Check for conflicts if requested
481
+ if (checkConflicts) {
482
+ const conflicts: Array<{
483
+ path1: string;
484
+ agent1: string;
485
+ path2: string;
486
+ agent2: string;
487
+ }> = [];
488
+
489
+ // Simple overlap detection - check if any patterns might conflict
490
+ for (let i = 0; i < reservations.length; i++) {
491
+ for (let j = i + 1; j < reservations.length; j++) {
492
+ const r1 = reservations[i];
493
+ const r2 = reservations[j];
494
+
495
+ // Skip same agent
496
+ if (r1.agent_name === r2.agent_name) continue;
497
+
498
+ // Check for potential overlap (simple heuristic)
499
+ const p1 = r1.path_pattern;
500
+ const p2 = r2.path_pattern;
501
+
502
+ // Glob pattern might overlap with specific file
503
+ if (
504
+ p1.includes("**") &&
505
+ p2.startsWith(p1.replace("/**", "").replace("**", ""))
506
+ ) {
507
+ conflicts.push({
508
+ path1: p1,
509
+ agent1: r1.agent_name,
510
+ path2: p2,
511
+ agent2: r2.agent_name,
512
+ });
513
+ } else if (
514
+ p2.includes("**") &&
515
+ p1.startsWith(p2.replace("/**", "").replace("**", ""))
516
+ ) {
517
+ conflicts.push({
518
+ path1: p2,
519
+ agent1: r2.agent_name,
520
+ path2: p1,
521
+ agent2: r1.agent_name,
522
+ });
523
+ }
524
+ }
525
+ }
526
+
527
+ result.conflicts = conflicts;
528
+ }
529
+
530
+ return result;
531
+ }
532
+
533
+ /**
534
+ * Get event timeline for visualization
535
+ */
536
+ export async function getEventTimeline(options: {
537
+ projectPath: string;
538
+ since?: number;
539
+ until?: number;
540
+ limit?: number;
541
+ }): Promise<TimelineResult> {
542
+ const { projectPath, since, until, limit = 100 } = options;
543
+
544
+ const events = await readEvents(
545
+ {
546
+ projectKey: projectPath,
547
+ since,
548
+ until,
549
+ limit,
550
+ },
551
+ projectPath,
552
+ );
553
+
554
+ // Sort by sequence ascending for timeline
555
+ events.sort((a, b) => a.sequence - b.sequence);
556
+
557
+ const timeline: TimelineEntry[] = events.map((e) => ({
558
+ time: formatTimestamp(e.timestamp),
559
+ type: e.type,
560
+ summary: summarizeEvent(e),
561
+ agent: getAgentFromEvent(e),
562
+ sequence: e.sequence,
563
+ }));
564
+
565
+ return { timeline };
566
+ }
567
+
568
+ /**
569
+ * Get complete state snapshot for debugging
570
+ */
571
+ export async function inspectState(
572
+ options: InspectStateOptions,
573
+ ): Promise<InspectStateResult> {
574
+ const { projectPath, format = "object" } = options;
575
+
576
+ const db = await getDatabase(projectPath);
577
+
578
+ // Get all agents
579
+ const agentsResult = await db.query<{
580
+ name: string;
581
+ program: string;
582
+ model: string;
583
+ task_description: string | null;
584
+ }>(
585
+ `SELECT name, program, model, task_description FROM agents WHERE project_key = $1`,
586
+ [projectPath],
587
+ );
588
+
589
+ // Get all messages
590
+ const messagesResult = await db.query<{
591
+ id: number;
592
+ from_agent: string;
593
+ subject: string;
594
+ thread_id: string | null;
595
+ }>(
596
+ `SELECT id, from_agent, subject, thread_id FROM messages WHERE project_key = $1`,
597
+ [projectPath],
598
+ );
599
+
600
+ // Get active reservations
601
+ const reservationsResult = await db.query<{
602
+ id: number;
603
+ agent_name: string;
604
+ path_pattern: string;
605
+ }>(
606
+ `SELECT id, agent_name, path_pattern FROM reservations
607
+ WHERE project_key = $1 AND released_at IS NULL AND expires_at > $2`,
608
+ [projectPath, Date.now()],
609
+ );
610
+
611
+ // Get stats
612
+ const stats = await getDatabaseStats(projectPath);
613
+ const latestSequence = await getLatestSequence(projectPath, projectPath);
614
+
615
+ const result: InspectStateResult = {
616
+ agents: agentsResult.rows,
617
+ messages: messagesResult.rows,
618
+ reservations: reservationsResult.rows,
619
+ eventCount: stats.events,
620
+ latestSequence,
621
+ stats,
622
+ };
623
+
624
+ if (format === "json") {
625
+ result.json = JSON.stringify(result, null, 2);
626
+ }
627
+
628
+ return result;
629
+ }