opencode-swarm-plugin 0.1.0 → 0.2.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.
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Run with: pnpm test:integration (or docker:test for full Docker environment)
9
9
  */
10
- import { describe, it, expect, beforeAll } from "vitest";
10
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
11
11
  import {
12
12
  swarm_decompose,
13
13
  swarm_validate_decomposition,
@@ -277,8 +277,8 @@ describe("swarm_subtask_prompt", () => {
277
277
  expect(result).toContain("Configure Google OAuth");
278
278
  expect(result).toContain("src/auth/google.ts");
279
279
  expect(result).toContain("NextAuth.js v5");
280
- expect(result).toContain("swarm:progress");
281
- expect(result).toContain("swarm:complete");
280
+ expect(result).toContain("swarm_progress");
281
+ expect(result).toContain("swarm_complete");
282
282
  });
283
283
 
284
284
  it("handles missing optional fields", async () => {
@@ -761,3 +761,194 @@ describe("full swarm flow (integration)", () => {
761
761
  },
762
762
  );
763
763
  });
764
+
765
+ // ============================================================================
766
+ // Tool Availability & Graceful Degradation Tests
767
+ // ============================================================================
768
+
769
+ import {
770
+ checkTool,
771
+ isToolAvailable,
772
+ checkAllTools,
773
+ formatToolAvailability,
774
+ resetToolCache,
775
+ withToolFallback,
776
+ ifToolAvailable,
777
+ } from "./tool-availability";
778
+ import { swarm_init } from "./swarm";
779
+
780
+ describe("Tool Availability", () => {
781
+ beforeAll(() => {
782
+ resetToolCache();
783
+ });
784
+
785
+ afterAll(() => {
786
+ resetToolCache();
787
+ });
788
+
789
+ it("checks individual tool availability", async () => {
790
+ const status = await checkTool("semantic-memory");
791
+ expect(status).toHaveProperty("available");
792
+ expect(status).toHaveProperty("checkedAt");
793
+ expect(typeof status.available).toBe("boolean");
794
+ });
795
+
796
+ it("caches tool availability checks", async () => {
797
+ const status1 = await checkTool("semantic-memory");
798
+ const status2 = await checkTool("semantic-memory");
799
+ // Same timestamp means cached
800
+ expect(status1.checkedAt).toBe(status2.checkedAt);
801
+ });
802
+
803
+ it("checks all tools at once", async () => {
804
+ const availability = await checkAllTools();
805
+ expect(availability.size).toBe(5);
806
+ expect(availability.has("semantic-memory")).toBe(true);
807
+ expect(availability.has("cass")).toBe(true);
808
+ expect(availability.has("ubs")).toBe(true);
809
+ expect(availability.has("beads")).toBe(true);
810
+ expect(availability.has("agent-mail")).toBe(true);
811
+ });
812
+
813
+ it("formats tool availability for display", async () => {
814
+ const availability = await checkAllTools();
815
+ const formatted = formatToolAvailability(availability);
816
+ expect(formatted).toContain("Tool Availability:");
817
+ expect(formatted).toContain("semantic-memory");
818
+ });
819
+
820
+ it("executes with fallback when tool unavailable", async () => {
821
+ // Force cache reset to test fresh
822
+ resetToolCache();
823
+
824
+ const result = await withToolFallback(
825
+ "ubs", // May or may not be available
826
+ async () => "action-result",
827
+ () => "fallback-result",
828
+ );
829
+
830
+ // Either result is valid depending on tool availability
831
+ expect(["action-result", "fallback-result"]).toContain(result);
832
+ });
833
+
834
+ it("returns undefined when tool unavailable with ifToolAvailable", async () => {
835
+ resetToolCache();
836
+
837
+ // This will return undefined if agent-mail is not running
838
+ const result = await ifToolAvailable("agent-mail", async () => "success");
839
+
840
+ // Result is either "success" or undefined
841
+ expect([undefined, "success"]).toContain(result);
842
+ });
843
+ });
844
+
845
+ describe("swarm_init", () => {
846
+ it("reports tool availability status", async () => {
847
+ resetToolCache();
848
+
849
+ const result = await swarm_init.execute({}, mockContext);
850
+ const parsed = JSON.parse(result);
851
+
852
+ expect(parsed).toHaveProperty("ready", true);
853
+ expect(parsed).toHaveProperty("tool_availability");
854
+ expect(parsed).toHaveProperty("report");
855
+
856
+ // Check tool availability structure
857
+ const tools = parsed.tool_availability;
858
+ expect(tools).toHaveProperty("semantic-memory");
859
+ expect(tools).toHaveProperty("cass");
860
+ expect(tools).toHaveProperty("ubs");
861
+ expect(tools).toHaveProperty("beads");
862
+ expect(tools).toHaveProperty("agent-mail");
863
+
864
+ // Each tool should have available and fallback
865
+ for (const [, info] of Object.entries(tools)) {
866
+ expect(info).toHaveProperty("available");
867
+ expect(info).toHaveProperty("fallback");
868
+ }
869
+ });
870
+
871
+ it("includes recommendations", async () => {
872
+ const result = await swarm_init.execute({}, mockContext);
873
+ const parsed = JSON.parse(result);
874
+
875
+ expect(parsed).toHaveProperty("recommendations");
876
+ expect(parsed.recommendations).toHaveProperty("beads");
877
+ expect(parsed.recommendations).toHaveProperty("agent_mail");
878
+ });
879
+ });
880
+
881
+ describe("Graceful Degradation", () => {
882
+ it("swarm_decompose works without CASS", async () => {
883
+ // This should work regardless of CASS availability
884
+ const result = await swarm_decompose.execute(
885
+ {
886
+ task: "Add user authentication",
887
+ max_subtasks: 3,
888
+ query_cass: true, // Request CASS but it may not be available
889
+ },
890
+ mockContext,
891
+ );
892
+
893
+ const parsed = JSON.parse(result);
894
+
895
+ // Should always return a valid prompt
896
+ expect(parsed).toHaveProperty("prompt");
897
+ expect(parsed.prompt).toContain("Add user authentication");
898
+
899
+ // CASS history should indicate whether it was queried
900
+ expect(parsed).toHaveProperty("cass_history");
901
+ expect(parsed.cass_history).toHaveProperty("queried");
902
+ });
903
+
904
+ it("swarm_decompose can skip CASS explicitly", async () => {
905
+ const result = await swarm_decompose.execute(
906
+ {
907
+ task: "Add user authentication",
908
+ max_subtasks: 3,
909
+ query_cass: false, // Explicitly skip CASS
910
+ },
911
+ mockContext,
912
+ );
913
+
914
+ const parsed = JSON.parse(result);
915
+
916
+ expect(parsed.cass_history.queried).toBe(false);
917
+ });
918
+
919
+ it("decomposition prompt includes beads discipline", async () => {
920
+ const result = await swarm_decompose.execute(
921
+ {
922
+ task: "Build feature X",
923
+ max_subtasks: 3,
924
+ },
925
+ mockContext,
926
+ );
927
+
928
+ const parsed = JSON.parse(result);
929
+
930
+ // Check that beads discipline is in the prompt
931
+ expect(parsed.prompt).toContain("MANDATORY");
932
+ expect(parsed.prompt).toContain("bead");
933
+ expect(parsed.prompt).toContain("Plan aggressively");
934
+ });
935
+
936
+ it("subtask prompt includes agent-mail discipline", async () => {
937
+ const result = await swarm_subtask_prompt.execute(
938
+ {
939
+ agent_name: "TestAgent",
940
+ bead_id: "bd-test123.1",
941
+ epic_id: "bd-test123",
942
+ subtask_title: "Test task",
943
+ files: ["src/test.ts"],
944
+ },
945
+ mockContext,
946
+ );
947
+
948
+ // Check that agent-mail discipline is in the prompt
949
+ expect(result).toContain("MANDATORY");
950
+ expect(result).toContain("Agent Mail");
951
+ expect(result).toContain("agentmail_send");
952
+ expect(result).toContain("Report progress");
953
+ });
954
+ });
package/src/swarm.ts CHANGED
@@ -35,6 +35,13 @@ import {
35
35
  type FeedbackEvent,
36
36
  DEFAULT_LEARNING_CONFIG,
37
37
  } from "./learning";
38
+ import {
39
+ isToolAvailable,
40
+ warnMissingTool,
41
+ checkAllTools,
42
+ formatToolAvailability,
43
+ type ToolName,
44
+ } from "./tool-availability";
38
45
 
39
46
  // ============================================================================
40
47
  // Conflict Detection
@@ -203,6 +210,18 @@ export const DECOMPOSITION_PROMPT = `You are decomposing a task into paralleliza
203
210
 
204
211
  {context_section}
205
212
 
213
+ ## MANDATORY: Beads Issue Tracking
214
+
215
+ **Every subtask MUST become a bead.** This is non-negotiable.
216
+
217
+ After decomposition, the coordinator will:
218
+ 1. Create an epic bead for the overall task
219
+ 2. Create child beads for each subtask
220
+ 3. Track progress through bead status updates
221
+ 4. Close beads with summaries when complete
222
+
223
+ Agents MUST update their bead status as they work. No silent progress.
224
+
206
225
  ## Requirements
207
226
 
208
227
  1. **Break into 2-{max_subtasks} independent subtasks** that can run in parallel
@@ -210,6 +229,7 @@ export const DECOMPOSITION_PROMPT = `You are decomposing a task into paralleliza
210
229
  3. **No file overlap** - files cannot appear in multiple subtasks (they get exclusive locks)
211
230
  4. **Order by dependency** - if subtask B needs subtask A's output, A must come first in the array
212
231
  5. **Estimate complexity** - 1 (trivial) to 5 (complex)
232
+ 6. **Plan aggressively** - break down more than you think necessary, smaller is better
213
233
 
214
234
  ## Response Format
215
235
 
@@ -236,10 +256,12 @@ Respond with a JSON object matching this schema:
236
256
 
237
257
  ## Guidelines
238
258
 
259
+ - **Plan aggressively** - when in doubt, split further. 3 small tasks > 1 medium task
239
260
  - **Prefer smaller, focused subtasks** over large complex ones
240
261
  - **Include test files** in the same subtask as the code they test
241
262
  - **Consider shared types** - if multiple files share types, handle that first
242
263
  - **Think about imports** - changes to exported APIs affect downstream files
264
+ - **Explicit > implicit** - spell out what each subtask should do, don't assume
243
265
 
244
266
  ## File Assignment Examples
245
267
 
@@ -277,19 +299,49 @@ send a message to the coordinator requesting the change.
277
299
  ## Shared Context
278
300
  {shared_context}
279
301
 
302
+ ## MANDATORY: Beads Tracking
303
+
304
+ You MUST keep your bead updated as you work:
305
+
306
+ 1. **Your bead is already in_progress** - don't change this unless blocked
307
+ 2. **If blocked**: \`bd update {bead_id} --status blocked\` and message coordinator
308
+ 3. **When done**: Use \`swarm_complete\` - it closes your bead automatically
309
+ 4. **Discovered issues**: Create new beads with \`bd create "issue" -t bug\`
310
+
311
+ **Never work silently.** Your bead status is how the swarm tracks progress.
312
+
313
+ ## MANDATORY: Agent Mail Communication
314
+
315
+ You MUST communicate with other agents:
316
+
317
+ 1. **Report progress** every significant milestone (not just at the end)
318
+ 2. **Ask questions** if requirements are unclear - don't guess
319
+ 3. **Announce blockers** immediately - don't spin trying to fix alone
320
+ 4. **Coordinate on shared concerns** - if you see something affecting other agents, say so
321
+
322
+ Use Agent Mail for all communication:
323
+ \`\`\`
324
+ agentmail_send(
325
+ to: ["coordinator" or specific agent],
326
+ subject: "Brief subject",
327
+ body: "Message content",
328
+ thread_id: "{epic_id}"
329
+ )
330
+ \`\`\`
331
+
280
332
  ## Coordination Protocol
281
333
 
282
334
  1. **Start**: Your bead is already marked in_progress
283
- 2. **Progress**: Use swarm:progress to report status updates
284
- 3. **Blocked**: If you hit a blocker, report it - don't spin
285
- 4. **Complete**: Use swarm:complete when done - it handles:
335
+ 2. **Progress**: Use swarm_progress to report status updates
336
+ 3. **Blocked**: Report immediately via Agent Mail - don't spin
337
+ 4. **Complete**: Use swarm_complete when done - it handles:
286
338
  - Closing your bead with a summary
287
339
  - Releasing file reservations
288
340
  - Notifying the coordinator
289
341
 
290
342
  ## Self-Evaluation
291
343
 
292
- Before calling swarm:complete, evaluate your work:
344
+ Before calling swarm_complete, evaluate your work:
293
345
  - Type safety: Does it compile without errors?
294
346
  - No obvious bugs: Did you handle edge cases?
295
347
  - Follows patterns: Does it match existing code style?
@@ -297,17 +349,13 @@ Before calling swarm:complete, evaluate your work:
297
349
 
298
350
  If evaluation fails, fix the issues before completing.
299
351
 
300
- ## Communication
352
+ ## Planning Your Work
301
353
 
302
- To message other agents or the coordinator:
303
- \`\`\`
304
- agent-mail:send(
305
- to: ["coordinator_name" or other agent],
306
- subject: "Brief subject",
307
- body: "Message content",
308
- thread_id: "{epic_id}"
309
- )
310
- \`\`\`
354
+ Before writing code:
355
+ 1. **Read the files** you're assigned to understand current state
356
+ 2. **Plan your approach** - what changes, in what order?
357
+ 3. **Identify risks** - what could go wrong? What dependencies?
358
+ 4. **Communicate your plan** via Agent Mail if non-trivial
311
359
 
312
360
  Begin work on your subtask now.`;
313
361
 
@@ -440,15 +488,23 @@ export function formatEvaluationPrompt(params: {
440
488
  * Query beads for subtasks of an epic
441
489
  */
442
490
  async function queryEpicSubtasks(epicId: string): Promise<Bead[]> {
491
+ // Check if beads is available
492
+ const beadsAvailable = await isToolAvailable("beads");
493
+ if (!beadsAvailable) {
494
+ warnMissingTool("beads");
495
+ return []; // Return empty - swarm can still function without status tracking
496
+ }
497
+
443
498
  const result = await Bun.$`bd list --parent ${epicId} --json`
444
499
  .quiet()
445
500
  .nothrow();
446
501
 
447
502
  if (result.exitCode !== 0) {
448
- throw new SwarmError(
449
- `Failed to query subtasks: ${result.stderr.toString()}`,
450
- "query_subtasks",
503
+ // Don't throw - just return empty and warn
504
+ console.warn(
505
+ `[swarm] Failed to query subtasks: ${result.stderr.toString()}`,
451
506
  );
507
+ return [];
452
508
  }
453
509
 
454
510
  try {
@@ -456,11 +512,8 @@ async function queryEpicSubtasks(epicId: string): Promise<Bead[]> {
456
512
  return z.array(BeadSchema).parse(parsed);
457
513
  } catch (error) {
458
514
  if (error instanceof z.ZodError) {
459
- throw new SwarmError(
460
- `Invalid bead data: ${error.message}`,
461
- "query_subtasks",
462
- error.issues,
463
- );
515
+ console.warn(`[swarm] Invalid bead data: ${error.message}`);
516
+ return [];
464
517
  }
465
518
  throw error;
466
519
  }
@@ -473,6 +526,13 @@ async function querySwarmMessages(
473
526
  projectKey: string,
474
527
  threadId: string,
475
528
  ): Promise<number> {
529
+ // Check if agent-mail is available
530
+ const agentMailAvailable = await isToolAvailable("agent-mail");
531
+ if (!agentMailAvailable) {
532
+ // Don't warn here - it's checked elsewhere
533
+ return 0;
534
+ }
535
+
476
536
  try {
477
537
  interface ThreadSummary {
478
538
  summary: { total_messages: number };
@@ -539,16 +599,18 @@ async function queryCassHistory(
539
599
  task: string,
540
600
  limit: number = 3,
541
601
  ): Promise<CassSearchResult | null> {
602
+ // Check if CASS is available first
603
+ const cassAvailable = await isToolAvailable("cass");
604
+ if (!cassAvailable) {
605
+ warnMissingTool("cass");
606
+ return null;
607
+ }
608
+
542
609
  try {
543
610
  const result = await Bun.$`cass search ${task} --limit ${limit} --json`
544
611
  .quiet()
545
612
  .nothrow();
546
613
 
547
- if (result.exitCode === 127) {
548
- // CASS not installed
549
- return null;
550
- }
551
-
552
614
  if (result.exitCode !== 0) {
553
615
  return null;
554
616
  }
@@ -1005,17 +1067,19 @@ async function runUbsScan(files: string[]): Promise<UbsScanResult | null> {
1005
1067
  return null;
1006
1068
  }
1007
1069
 
1070
+ // Check if UBS is available first
1071
+ const ubsAvailable = await isToolAvailable("ubs");
1072
+ if (!ubsAvailable) {
1073
+ warnMissingTool("ubs");
1074
+ return null;
1075
+ }
1076
+
1008
1077
  try {
1009
1078
  // Run UBS scan with JSON output
1010
1079
  const result = await Bun.$`ubs scan ${files.join(" ")} --json`
1011
1080
  .quiet()
1012
1081
  .nothrow();
1013
1082
 
1014
- if (result.exitCode === 127) {
1015
- // UBS not installed
1016
- return null;
1017
- }
1018
-
1019
1083
  const output = result.stdout.toString();
1020
1084
  if (!output.trim()) {
1021
1085
  return {
@@ -1395,11 +1459,100 @@ export const swarm_evaluation_prompt = tool({
1395
1459
  },
1396
1460
  });
1397
1461
 
1462
+ /**
1463
+ * Initialize swarm and check tool availability
1464
+ *
1465
+ * Call this at the start of a swarm session to see what tools are available
1466
+ * and what features will be degraded.
1467
+ */
1468
+ export const swarm_init = tool({
1469
+ description:
1470
+ "Initialize swarm session and check tool availability. Call at swarm start to see what features are available.",
1471
+ args: {
1472
+ project_path: tool.schema
1473
+ .string()
1474
+ .optional()
1475
+ .describe("Project path (for Agent Mail init)"),
1476
+ },
1477
+ async execute(args) {
1478
+ // Check all tools
1479
+ const availability = await checkAllTools();
1480
+
1481
+ // Build status report
1482
+ const report = formatToolAvailability(availability);
1483
+
1484
+ // Check critical tools
1485
+ const beadsAvailable = availability.get("beads")?.status.available ?? false;
1486
+ const agentMailAvailable =
1487
+ availability.get("agent-mail")?.status.available ?? false;
1488
+
1489
+ // Build warnings
1490
+ const warnings: string[] = [];
1491
+ const degradedFeatures: string[] = [];
1492
+
1493
+ if (!beadsAvailable) {
1494
+ warnings.push(
1495
+ "⚠️ beads (bd) not available - issue tracking disabled, swarm coordination will be limited",
1496
+ );
1497
+ degradedFeatures.push("issue tracking", "progress persistence");
1498
+ }
1499
+
1500
+ if (!agentMailAvailable) {
1501
+ warnings.push(
1502
+ "⚠️ agent-mail not available - multi-agent communication disabled",
1503
+ );
1504
+ degradedFeatures.push("agent communication", "file reservations");
1505
+ }
1506
+
1507
+ if (!availability.get("cass")?.status.available) {
1508
+ degradedFeatures.push("historical context from past sessions");
1509
+ }
1510
+
1511
+ if (!availability.get("ubs")?.status.available) {
1512
+ degradedFeatures.push("pre-completion bug scanning");
1513
+ }
1514
+
1515
+ if (!availability.get("semantic-memory")?.status.available) {
1516
+ degradedFeatures.push("persistent learning (using in-memory fallback)");
1517
+ }
1518
+
1519
+ return JSON.stringify(
1520
+ {
1521
+ ready: true,
1522
+ tool_availability: Object.fromEntries(
1523
+ Array.from(availability.entries()).map(([k, v]) => [
1524
+ k,
1525
+ {
1526
+ available: v.status.available,
1527
+ fallback: v.status.available ? null : v.fallbackBehavior,
1528
+ },
1529
+ ]),
1530
+ ),
1531
+ warnings: warnings.length > 0 ? warnings : undefined,
1532
+ degraded_features:
1533
+ degradedFeatures.length > 0 ? degradedFeatures : undefined,
1534
+ recommendations: {
1535
+ beads: beadsAvailable
1536
+ ? "✓ Use beads for all task tracking"
1537
+ : "Install beads: npm i -g @joelhooks/beads",
1538
+ agent_mail: agentMailAvailable
1539
+ ? "✓ Use Agent Mail for coordination"
1540
+ : "Start Agent Mail: agent-mail serve",
1541
+ },
1542
+ report,
1543
+ },
1544
+ null,
1545
+ 2,
1546
+ );
1547
+ },
1548
+ });
1549
+
1398
1550
  // ============================================================================
1399
1551
  // Export all tools
1400
1552
  // ============================================================================
1401
1553
 
1402
1554
  export const swarmTools = {
1555
+ swarm_init: swarm_init,
1403
1556
  swarm_decompose: swarm_decompose,
1404
1557
  swarm_validate_decomposition: swarm_validate_decomposition,
1405
1558
  swarm_status: swarm_status,