opencode-swarm-plugin 0.12.30 → 0.13.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 (48) 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/bin/swarm.ts +120 -31
  5. package/bun.lock +23 -0
  6. package/dist/index.js +4020 -438
  7. package/dist/pglite.data +0 -0
  8. package/dist/pglite.wasm +0 -0
  9. package/dist/plugin.js +4008 -514
  10. package/examples/commands/swarm.md +114 -19
  11. package/examples/skills/beads-workflow/SKILL.md +75 -28
  12. package/examples/skills/swarm-coordination/SKILL.md +92 -1
  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.ts +84 -59
  47. package/src/tool-availability.ts +35 -2
  48. package/global-skills/mcp-tool-authoring/SKILL.md +0 -695
@@ -1,695 +0,0 @@
1
- ---
2
- name: mcp-tool-authoring
3
- description: Building MCP (Model Context Protocol) tools for OpenCode plugins. Use when creating new tools, defining tool schemas, handling tool arguments, or extending the swarm plugin. Covers schema definition, context passing, error handling, and tool registration.
4
- tags: [mcp, opencode, plugins, api-design, tool-development]
5
- ---
6
-
7
- # MCP Tool Authoring
8
-
9
- Build type-safe MCP tools for OpenCode plugins using the `@opencode-ai/plugin` SDK.
10
-
11
- ## Tool Definition Pattern
12
-
13
- Define tools with `tool()` helper from `@opencode-ai/plugin`:
14
-
15
- ```typescript
16
- import { tool } from "@opencode-ai/plugin";
17
-
18
- export const my_tool = tool({
19
- description: "Clear, concise description (one sentence, action-focused)",
20
- args: {
21
- required_arg: tool.schema.string().describe("What this arg does"),
22
- optional_arg: tool.schema.number().optional().describe("Optional arg"),
23
- },
24
- async execute(args, ctx) {
25
- // Implementation
26
- return "Success message or JSON output";
27
- },
28
- });
29
- ```
30
-
31
- **Key rules:**
32
-
33
- - Description: Imperative form, under 120 chars. Start with verb.
34
- - Args: Use `tool.schema` for validation (Zod-like API)
35
- - Execute: Return string or JSON-serializable value
36
- - Context (`ctx`): Contains `sessionID`, `messageID`, `agent` for state tracking
37
-
38
- ## Schema Definition
39
-
40
- Use `tool.schema` for type-safe argument validation:
41
-
42
- ### Primitives
43
-
44
- ```typescript
45
- tool.schema.string(); // string
46
- tool.schema.number(); // number
47
- tool.schema.boolean(); // boolean
48
- ```
49
-
50
- ### Constraints
51
-
52
- ```typescript
53
- tool.schema.string().min(1); // non-empty string
54
- tool.schema.number().min(0).max(10); // range validation
55
- tool.schema.number().int(); // integer only
56
- tool.schema.enum(["a", "b", "c"]); // enum values
57
- ```
58
-
59
- ### Complex Types
60
-
61
- ```typescript
62
- // Array
63
- tool.schema.array(tool.schema.string());
64
-
65
- // Object
66
- tool.schema.object({
67
- name: tool.schema.string(),
68
- age: tool.schema.number().optional(),
69
- });
70
-
71
- // Nested
72
- tool.schema.array(
73
- tool.schema.object({
74
- title: tool.schema.string(),
75
- priority: tool.schema.number().min(0).max(3),
76
- }),
77
- );
78
- ```
79
-
80
- ### Optional Arguments
81
-
82
- ```typescript
83
- args: {
84
- required: tool.schema.string().describe("Must provide"),
85
- optional: tool.schema.string().optional().describe("Can omit"),
86
- }
87
- ```
88
-
89
- ## Context Passing
90
-
91
- Every tool receives `ctx` with session metadata:
92
-
93
- ```typescript
94
- interface ToolContext {
95
- sessionID: string; // Unique session identifier
96
- messageID: string; // Current message ID
97
- agent: string; // Agent name (e.g., "Claude Code")
98
- }
99
-
100
- async execute(args, ctx) {
101
- const { sessionID, messageID, agent } = ctx;
102
-
103
- // Use sessionID for state persistence across tool calls
104
- // Use messageID for tracing/logging
105
- // Use agent for multi-agent coordination
106
- }
107
- ```
108
-
109
- ### Session State Pattern
110
-
111
- Store state keyed by `sessionID` for multi-call workflows:
112
-
113
- ```typescript
114
- const sessionStates = new Map<string, SessionState>();
115
-
116
- function requireState(sessionID: string): SessionState {
117
- const state = sessionStates.get(sessionID);
118
- if (!state) {
119
- throw new Error("Not initialized - call init first");
120
- }
121
- return state;
122
- }
123
-
124
- export const init_tool = tool({
125
- args: {
126
- /* ... */
127
- },
128
- async execute(args, ctx) {
129
- const state = {
130
- /* ... */
131
- };
132
- sessionStates.set(ctx.sessionID, state);
133
- return "Initialized";
134
- },
135
- });
136
-
137
- export const action_tool = tool({
138
- args: {
139
- /* ... */
140
- },
141
- async execute(args, ctx) {
142
- const state = requireState(ctx.sessionID);
143
- // Use state
144
- },
145
- });
146
- ```
147
-
148
- ### Persistent State (CLI Bridge)
149
-
150
- For CLI-based tools, persist state to disk:
151
-
152
- ```typescript
153
- import { existsSync, readFileSync, writeFileSync } from "fs";
154
- import { join } from "path";
155
- import { tmpdir } from "os";
156
-
157
- const STATE_DIR = join(tmpdir(), "my-plugin-sessions");
158
-
159
- function loadState(sessionID: string): State | null {
160
- const path = join(STATE_DIR, `${sessionID}.json`);
161
- if (existsSync(path)) {
162
- return JSON.parse(readFileSync(path, "utf-8"));
163
- }
164
- return null;
165
- }
166
-
167
- function saveState(sessionID: string, state: State): void {
168
- mkdirSync(STATE_DIR, { recursive: true });
169
- const path = join(STATE_DIR, `${sessionID}.json`);
170
- writeFileSync(path, JSON.stringify(state, null, 2));
171
- }
172
- ```
173
-
174
- ## Error Handling
175
-
176
- Return errors as strings vs throwing for different behaviors:
177
-
178
- ### Throw for Hard Failures
179
-
180
- Agent sees error, cannot continue with invalid result:
181
-
182
- ```typescript
183
- async execute(args, ctx) {
184
- if (!args.required_field) {
185
- throw new Error("Missing required_field");
186
- }
187
-
188
- const result = await riskyOperation();
189
- if (!result) {
190
- throw new Error("Operation failed - cannot proceed");
191
- }
192
-
193
- return result;
194
- }
195
- ```
196
-
197
- ### Return for Graceful Degradation
198
-
199
- Agent gets error message but can decide how to handle:
200
-
201
- ```typescript
202
- async execute(args, ctx) {
203
- try {
204
- return await preferredMethod();
205
- } catch (error) {
206
- // Return fallback instead of throwing
207
- return JSON.stringify({
208
- available: false,
209
- error: error.message,
210
- fallback: "Use alternative approach",
211
- });
212
- }
213
- }
214
- ```
215
-
216
- ### Custom Error Classes
217
-
218
- Type-safe errors with metadata:
219
-
220
- ```typescript
221
- export class ToolError extends Error {
222
- constructor(
223
- message: string,
224
- public readonly code?: number,
225
- public readonly data?: unknown,
226
- ) {
227
- super(message);
228
- this.name = "ToolError";
229
- Object.setPrototypeOf(this, ToolError.prototype);
230
- }
231
- }
232
-
233
- async execute(args, ctx) {
234
- if (invalid) {
235
- throw new ToolError("Invalid input", 400, { field: "name" });
236
- }
237
- }
238
- ```
239
-
240
- ## Tool Registration
241
-
242
- Export tools in plugin hooks:
243
-
244
- ```typescript
245
- import type { Plugin, PluginInput, Hooks } from "@opencode-ai/plugin";
246
- import { my_tool, another_tool } from "./tools";
247
-
248
- export const MyPlugin: Plugin = async (input: PluginInput): Promise<Hooks> => {
249
- return {
250
- tool: {
251
- my_tool,
252
- another_tool,
253
- },
254
- };
255
- };
256
-
257
- export default MyPlugin;
258
- ```
259
-
260
- ### Namespace Pattern
261
-
262
- Group related tools:
263
-
264
- ```typescript
265
- import { beadsTools } from "./beads";
266
- import { swarmTools } from "./swarm";
267
-
268
- export const SwarmPlugin: Plugin = async (input) => {
269
- return {
270
- tool: {
271
- ...beadsTools, // beads_create, beads_query, etc.
272
- ...swarmTools, // swarm_decompose, swarm_status, etc.
273
- },
274
- };
275
- };
276
- ```
277
-
278
- ### Tool Lifecycle Hooks
279
-
280
- Execute code before/after tool calls:
281
-
282
- ```typescript
283
- export const MyPlugin: Plugin = async (input) => {
284
- return {
285
- tool: {
286
- /* tools */
287
- },
288
-
289
- // After tool execution
290
- "tool.execute.after": async (input, output) => {
291
- const toolName = input.tool;
292
-
293
- if (toolName === "close_task") {
294
- // Auto-cleanup
295
- await runCleanup();
296
- }
297
-
298
- if (toolName === "init") {
299
- // Track state
300
- trackInitialization(output.output);
301
- }
302
- },
303
-
304
- // Session lifecycle
305
- event: async ({ event }) => {
306
- if (event.type === "session.idle") {
307
- // Release resources
308
- await cleanup();
309
- }
310
- },
311
- };
312
- };
313
- ```
314
-
315
- ## CLI Bridge Pattern
316
-
317
- Delegate execution to external CLI (common for complex tools):
318
-
319
- ```typescript
320
- import { spawn } from "child_process";
321
-
322
- async function execCLI(
323
- command: string,
324
- args: string[],
325
- ctx: ToolContext,
326
- ): Promise<string> {
327
- return new Promise((resolve, reject) => {
328
- const proc = spawn(command, args, {
329
- stdio: ["ignore", "pipe", "pipe"],
330
- env: {
331
- ...process.env,
332
- TOOL_SESSION_ID: ctx.sessionID,
333
- TOOL_MESSAGE_ID: ctx.messageID,
334
- TOOL_AGENT: ctx.agent,
335
- },
336
- });
337
-
338
- let stdout = "";
339
- let stderr = "";
340
-
341
- proc.stdout.on("data", (data) => {
342
- stdout += data;
343
- });
344
- proc.stderr.on("data", (data) => {
345
- stderr += data;
346
- });
347
-
348
- proc.on("close", (code) => {
349
- if (code === 0) {
350
- resolve(stdout);
351
- } else {
352
- reject(new Error(stderr || `Exit ${code}`));
353
- }
354
- });
355
-
356
- proc.on("error", (err) => {
357
- reject(err);
358
- });
359
- });
360
- }
361
-
362
- export const cli_tool = tool({
363
- description: "Execute via CLI",
364
- args: {
365
- arg: tool.schema.string(),
366
- },
367
- async execute(args, ctx) {
368
- const output = await execCLI("my-cli", ["--arg", args.arg], ctx);
369
- return output;
370
- },
371
- });
372
- ```
373
-
374
- ### JSON Communication
375
-
376
- For structured CLI responses:
377
-
378
- ```typescript
379
- async execute(args, ctx) {
380
- const cliArgs = ["tool", "name", "--json", JSON.stringify(args)];
381
- const output = await execCLI("my-cli", cliArgs, ctx);
382
-
383
- try {
384
- const result = JSON.parse(output);
385
- if (result.success) {
386
- return JSON.stringify(result.data, null, 2);
387
- } else {
388
- throw new Error(result.error || "CLI failed");
389
- }
390
- } catch {
391
- // Not JSON - return raw
392
- return output;
393
- }
394
- }
395
- ```
396
-
397
- ## Testing Tools
398
-
399
- Test tools outside OpenCode runtime:
400
-
401
- ```typescript
402
- import { describe, it, expect } from "vitest";
403
- import { my_tool } from "./my-tool";
404
-
405
- describe("my_tool", () => {
406
- it("validates required args", async () => {
407
- const ctx = {
408
- sessionID: "test-session",
409
- messageID: "test-msg",
410
- agent: "test-agent",
411
- };
412
-
413
- await expect(
414
- my_tool.execute(
415
- {
416
- /* missing required */
417
- },
418
- ctx,
419
- ),
420
- ).rejects.toThrow();
421
- });
422
-
423
- it("returns success for valid input", async () => {
424
- const ctx = { sessionID: "test", messageID: "msg", agent: "agent" };
425
- const result = await my_tool.execute({ arg: "value" }, ctx);
426
-
427
- expect(result).toContain("Success");
428
- });
429
- });
430
- ```
431
-
432
- ### Mock Context
433
-
434
- Reusable test context:
435
-
436
- ```typescript
437
- function mockContext(overrides?: Partial<ToolContext>): ToolContext {
438
- return {
439
- sessionID: "test-session",
440
- messageID: "test-message",
441
- agent: "test-agent",
442
- ...overrides,
443
- };
444
- }
445
-
446
- it("uses session state", async () => {
447
- const ctx = mockContext({ sessionID: "unique-session" });
448
- await init_tool.execute(
449
- {
450
- /* ... */
451
- },
452
- ctx,
453
- );
454
- const result = await action_tool.execute(
455
- {
456
- /* ... */
457
- },
458
- ctx,
459
- );
460
- expect(result).toBeDefined();
461
- });
462
- ```
463
-
464
- ## Best Practices
465
-
466
- ### Descriptions
467
-
468
- ```typescript
469
- // ✅ Good: Action-focused, under 120 chars
470
- "Create a new bead with type-safe validation";
471
- "Query beads with filters (replaces bd list, bd ready, bd wip)";
472
-
473
- // ❌ Bad: Vague, too long
474
- "This tool helps you to create beads in the system";
475
- "Query the beads database using various filtering mechanisms...";
476
- ```
477
-
478
- ### Arguments
479
-
480
- ```typescript
481
- // ✅ Good: Descriptive, type-constrained
482
- {
483
- title: tool.schema.string().min(1).describe("Bead title"),
484
- priority: tool.schema.number().min(0).max(3).optional().describe("Priority 0-3"),
485
- }
486
-
487
- // ❌ Bad: No validation, unclear
488
- {
489
- title: tool.schema.string(),
490
- priority: tool.schema.number(),
491
- }
492
- ```
493
-
494
- ### Return Values
495
-
496
- ```typescript
497
- // ✅ Good: Consistent JSON for structured data
498
- return JSON.stringify({ id: "bd-123", status: "open" }, null, 2);
499
-
500
- // ✅ Good: Human-readable for simple operations
501
- return "Created bead bd-123";
502
-
503
- // ❌ Bad: Inconsistent format
504
- return Math.random() > 0.5 ? { id: "bd-123" } : "Created";
505
- ```
506
-
507
- ### Error Messages
508
-
509
- ```typescript
510
- // ✅ Good: Actionable, specific
511
- throw new Error("Agent Mail not initialized. Call agentmail_init first.");
512
- throw new ToolError("Invalid priority: must be 0-3", 400, {
513
- value: args.priority,
514
- });
515
-
516
- // ❌ Bad: Vague, no context
517
- throw new Error("Failed");
518
- throw new Error("Error");
519
- ```
520
-
521
- ## Common Patterns
522
-
523
- ### Validation Before Execution
524
-
525
- ```typescript
526
- import { z } from "zod";
527
-
528
- const ArgsSchema = z.object({
529
- id: z.string().min(1),
530
- priority: z.number().min(0).max(3).optional(),
531
- });
532
-
533
- async execute(args, ctx) {
534
- const validated = ArgsSchema.parse(args);
535
- // Type-safe: validated.id is string, validated.priority is number | undefined
536
- }
537
- ```
538
-
539
- ### Rate Limiting
540
-
541
- ```typescript
542
- const rateLimiter = new Map<string, { count: number; resetAt: number }>();
543
-
544
- async execute(args, ctx) {
545
- const limit = rateLimiter.get(ctx.sessionID);
546
- const now = Date.now();
547
-
548
- if (limit && limit.resetAt > now) {
549
- if (limit.count >= 100) {
550
- throw new Error(`Rate limit exceeded. Retry after ${new Date(limit.resetAt)}`);
551
- }
552
- limit.count++;
553
- } else {
554
- rateLimiter.set(ctx.sessionID, {
555
- count: 1,
556
- resetAt: now + 60_000, // 1 minute
557
- });
558
- }
559
-
560
- // Execute
561
- }
562
- ```
563
-
564
- ### Retry Logic
565
-
566
- ```typescript
567
- async function withRetry<T>(
568
- fn: () => Promise<T>,
569
- maxRetries = 3,
570
- ): Promise<T> {
571
- let lastError: Error | null = null;
572
-
573
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
574
- try {
575
- return await fn();
576
- } catch (error) {
577
- lastError = error instanceof Error ? error : new Error(String(error));
578
-
579
- if (attempt < maxRetries) {
580
- const delay = Math.pow(2, attempt) * 100;
581
- await new Promise((resolve) => setTimeout(resolve, delay));
582
- }
583
- }
584
- }
585
-
586
- throw lastError;
587
- }
588
-
589
- async execute(args, ctx) {
590
- return await withRetry(() => riskyOperation(args));
591
- }
592
- ```
593
-
594
- ### Graceful Degradation
595
-
596
- ```typescript
597
- async execute(args, ctx) {
598
- // Try preferred method
599
- const available = await checkDependency();
600
-
601
- if (!available) {
602
- // Return fallback info instead of failing
603
- return JSON.stringify({
604
- available: false,
605
- error: "Dependency not running",
606
- fallback: "Install with: npm install -g dependency",
607
- });
608
- }
609
-
610
- // Normal execution
611
- const result = await callDependency(args);
612
- return JSON.stringify({ available: true, result });
613
- }
614
- ```
615
-
616
- ## Anti-Patterns
617
-
618
- ### ❌ Overloaded Tools
619
-
620
- Don't combine unrelated actions in one tool:
621
-
622
- ```typescript
623
- // BAD: Does too much
624
- const manage_item = tool({
625
- args: {
626
- action: tool.schema.enum(["create", "update", "delete", "list"]),
627
- // ...
628
- },
629
- });
630
-
631
- // GOOD: Separate tools
632
- const create_item = tool({
633
- /* ... */
634
- });
635
- const update_item = tool({
636
- /* ... */
637
- });
638
- const delete_item = tool({
639
- /* ... */
640
- });
641
- ```
642
-
643
- ### ❌ Hidden State
644
-
645
- Don't rely on module-level state without sessionID:
646
-
647
- ```typescript
648
- // BAD: Shared across sessions
649
- let currentUser: string;
650
-
651
- export const set_user = tool({
652
- async execute(args) {
653
- currentUser = args.name; // Leaks between sessions!
654
- },
655
- });
656
-
657
- // GOOD: Session-keyed state
658
- const sessions = new Map<string, { user: string }>();
659
-
660
- export const set_user = tool({
661
- async execute(args, ctx) {
662
- sessions.set(ctx.sessionID, { user: args.name });
663
- },
664
- });
665
- ```
666
-
667
- ### ❌ Swallowing Errors
668
-
669
- Don't hide errors from the agent:
670
-
671
- ```typescript
672
- // BAD: Silent failure
673
- async execute(args, ctx) {
674
- try {
675
- return await criticalOperation();
676
- } catch {
677
- return "Done"; // Lies!
678
- }
679
- }
680
-
681
- // GOOD: Explicit error or fallback
682
- async execute(args, ctx) {
683
- try {
684
- return await criticalOperation();
685
- } catch (error) {
686
- throw new Error(`Critical operation failed: ${error.message}`);
687
- }
688
- }
689
- ```
690
-
691
- ## Related
692
-
693
- - OpenCode Plugin SDK: `@opencode-ai/plugin`
694
- - Zod validation: [github.com/colinhacks/zod](https://github.com/colinhacks/zod)
695
- - MCP spec: [modelcontextprotocol.io](https://modelcontextprotocol.io)