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,302 @@
1
+ /**
2
+ * Swarm Mail Projections Layer - Query materialized views
3
+ *
4
+ * Projections are the read-side of CQRS. They query denormalized
5
+ * materialized views for fast reads. Views are updated by the
6
+ * event store when events are appended.
7
+ *
8
+ * Key projections:
9
+ * - getAgents: List registered agents
10
+ * - getInbox: Get messages for an agent
11
+ * - getActiveReservations: Get current file locks
12
+ * - checkConflicts: Detect reservation conflicts
13
+ */
14
+ import { getDatabase } from "./index";
15
+ import { minimatch } from "minimatch";
16
+
17
+ // ============================================================================
18
+ // Types
19
+ // ============================================================================
20
+
21
+ export interface Agent {
22
+ id: number;
23
+ name: string;
24
+ program: string;
25
+ model: string;
26
+ task_description: string | null;
27
+ registered_at: number;
28
+ last_active_at: number;
29
+ }
30
+
31
+ export interface Message {
32
+ id: number;
33
+ from_agent: string;
34
+ subject: string;
35
+ body?: string;
36
+ thread_id: string | null;
37
+ importance: string;
38
+ ack_required: boolean;
39
+ created_at: number;
40
+ read_at?: number | null;
41
+ acked_at?: number | null;
42
+ }
43
+
44
+ export interface Reservation {
45
+ id: number;
46
+ agent_name: string;
47
+ path_pattern: string;
48
+ exclusive: boolean;
49
+ reason: string | null;
50
+ created_at: number;
51
+ expires_at: number;
52
+ }
53
+
54
+ export interface Conflict {
55
+ path: string;
56
+ holder: string;
57
+ pattern: string;
58
+ exclusive: boolean;
59
+ }
60
+
61
+ // ============================================================================
62
+ // Agent Projections
63
+ // ============================================================================
64
+
65
+ /**
66
+ * Get all agents for a project
67
+ */
68
+ export async function getAgents(
69
+ projectKey: string,
70
+ projectPath?: string,
71
+ ): Promise<Agent[]> {
72
+ const db = await getDatabase(projectPath);
73
+
74
+ const result = await db.query<Agent>(
75
+ `SELECT id, name, program, model, task_description, registered_at, last_active_at
76
+ FROM agents
77
+ WHERE project_key = $1
78
+ ORDER BY registered_at ASC`,
79
+ [projectKey],
80
+ );
81
+
82
+ return result.rows;
83
+ }
84
+
85
+ /**
86
+ * Get a specific agent by name
87
+ */
88
+ export async function getAgent(
89
+ projectKey: string,
90
+ agentName: string,
91
+ projectPath?: string,
92
+ ): Promise<Agent | null> {
93
+ const db = await getDatabase(projectPath);
94
+
95
+ const result = await db.query<Agent>(
96
+ `SELECT id, name, program, model, task_description, registered_at, last_active_at
97
+ FROM agents
98
+ WHERE project_key = $1 AND name = $2`,
99
+ [projectKey, agentName],
100
+ );
101
+
102
+ return result.rows[0] ?? null;
103
+ }
104
+
105
+ // ============================================================================
106
+ // Message Projections
107
+ // ============================================================================
108
+
109
+ export interface InboxOptions {
110
+ limit?: number;
111
+ urgentOnly?: boolean;
112
+ unreadOnly?: boolean;
113
+ includeBodies?: boolean;
114
+ sinceTs?: string;
115
+ }
116
+
117
+ /**
118
+ * Get inbox messages for an agent
119
+ */
120
+ export async function getInbox(
121
+ projectKey: string,
122
+ agentName: string,
123
+ options: InboxOptions = {},
124
+ projectPath?: string,
125
+ ): Promise<Message[]> {
126
+ const db = await getDatabase(projectPath);
127
+
128
+ const {
129
+ limit = 50,
130
+ urgentOnly = false,
131
+ unreadOnly = false,
132
+ includeBodies = true,
133
+ } = options;
134
+
135
+ // Build query with conditions
136
+ const conditions = ["m.project_key = $1", "mr.agent_name = $2"];
137
+ const params: (string | number)[] = [projectKey, agentName];
138
+ let paramIndex = 3;
139
+
140
+ if (urgentOnly) {
141
+ conditions.push(`m.importance = 'urgent'`);
142
+ }
143
+
144
+ if (unreadOnly) {
145
+ conditions.push(`mr.read_at IS NULL`);
146
+ }
147
+
148
+ const bodySelect = includeBodies ? ", m.body" : "";
149
+
150
+ const query = `
151
+ SELECT m.id, m.from_agent, m.subject${bodySelect}, m.thread_id,
152
+ m.importance, m.ack_required, m.created_at,
153
+ mr.read_at, mr.acked_at
154
+ FROM messages m
155
+ JOIN message_recipients mr ON m.id = mr.message_id
156
+ WHERE ${conditions.join(" AND ")}
157
+ ORDER BY m.created_at DESC
158
+ LIMIT $${paramIndex}
159
+ `;
160
+ params.push(limit);
161
+
162
+ const result = await db.query<Message>(query, params);
163
+
164
+ return result.rows;
165
+ }
166
+
167
+ /**
168
+ * Get a single message by ID with full body
169
+ */
170
+ export async function getMessage(
171
+ projectKey: string,
172
+ messageId: number,
173
+ projectPath?: string,
174
+ ): Promise<Message | null> {
175
+ const db = await getDatabase(projectPath);
176
+
177
+ const result = await db.query<Message>(
178
+ `SELECT id, from_agent, subject, body, thread_id, importance, ack_required, created_at
179
+ FROM messages
180
+ WHERE project_key = $1 AND id = $2`,
181
+ [projectKey, messageId],
182
+ );
183
+
184
+ return result.rows[0] ?? null;
185
+ }
186
+
187
+ /**
188
+ * Get all messages in a thread
189
+ */
190
+ export async function getThreadMessages(
191
+ projectKey: string,
192
+ threadId: string,
193
+ projectPath?: string,
194
+ ): Promise<Message[]> {
195
+ const db = await getDatabase(projectPath);
196
+
197
+ const result = await db.query<Message>(
198
+ `SELECT id, from_agent, subject, body, thread_id, importance, ack_required, created_at
199
+ FROM messages
200
+ WHERE project_key = $1 AND thread_id = $2
201
+ ORDER BY created_at ASC`,
202
+ [projectKey, threadId],
203
+ );
204
+
205
+ return result.rows;
206
+ }
207
+
208
+ // ============================================================================
209
+ // Reservation Projections
210
+ // ============================================================================
211
+
212
+ /**
213
+ * Get active (non-expired, non-released) reservations
214
+ */
215
+ export async function getActiveReservations(
216
+ projectKey: string,
217
+ projectPath?: string,
218
+ agentName?: string,
219
+ ): Promise<Reservation[]> {
220
+ const db = await getDatabase(projectPath);
221
+
222
+ const now = Date.now();
223
+ const baseQuery = `
224
+ SELECT id, agent_name, path_pattern, exclusive, reason, created_at, expires_at
225
+ FROM reservations
226
+ WHERE project_key = $1
227
+ AND released_at IS NULL
228
+ AND expires_at > $2
229
+ `;
230
+ const params: (string | number)[] = [projectKey, now];
231
+ let query = baseQuery;
232
+
233
+ if (agentName) {
234
+ query += ` AND agent_name = $3`;
235
+ params.push(agentName);
236
+ }
237
+
238
+ query += ` ORDER BY created_at ASC`;
239
+
240
+ const result = await db.query<Reservation>(query, params);
241
+
242
+ return result.rows;
243
+ }
244
+
245
+ /**
246
+ * Check for conflicts with existing reservations
247
+ *
248
+ * Returns conflicts where:
249
+ * - Another agent holds an exclusive reservation
250
+ * - The path matches (exact or glob pattern)
251
+ * - The reservation is still active
252
+ */
253
+ export async function checkConflicts(
254
+ projectKey: string,
255
+ agentName: string,
256
+ paths: string[],
257
+ projectPath?: string,
258
+ ): Promise<Conflict[]> {
259
+ // Get all active exclusive reservations from OTHER agents
260
+ const reservations = await getActiveReservations(projectKey, projectPath);
261
+
262
+ const conflicts: Conflict[] = [];
263
+
264
+ for (const reservation of reservations) {
265
+ // Skip own reservations
266
+ if (reservation.agent_name === agentName) {
267
+ continue;
268
+ }
269
+
270
+ // Skip non-exclusive reservations
271
+ if (!reservation.exclusive) {
272
+ continue;
273
+ }
274
+
275
+ // Check each requested path against the reservation pattern
276
+ for (const path of paths) {
277
+ if (pathMatches(path, reservation.path_pattern)) {
278
+ conflicts.push({
279
+ path,
280
+ holder: reservation.agent_name,
281
+ pattern: reservation.path_pattern,
282
+ exclusive: reservation.exclusive,
283
+ });
284
+ }
285
+ }
286
+ }
287
+
288
+ return conflicts;
289
+ }
290
+
291
+ /**
292
+ * Check if a path matches a pattern (supports glob patterns)
293
+ */
294
+ function pathMatches(path: string, pattern: string): boolean {
295
+ // Exact match
296
+ if (path === pattern) {
297
+ return true;
298
+ }
299
+
300
+ // Glob match using minimatch
301
+ return minimatch(path, pattern);
302
+ }