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
package/src/swarm.ts CHANGED
@@ -25,7 +25,12 @@ import {
25
25
  type SpawnedAgent,
26
26
  type Bead,
27
27
  } from "./schemas";
28
- import { mcpCall, mcpCallWithAutoInit, requireState } from "./agent-mail";
28
+ import {
29
+ sendSwarmMessage,
30
+ getSwarmInbox,
31
+ readSwarmMessage,
32
+ releaseSwarmFiles,
33
+ } from "./streams/swarm-mail";
29
34
  import {
30
35
  OutcomeSignalsSchema,
31
36
  DecompositionStrategySchema,
@@ -959,15 +964,19 @@ async function querySwarmMessages(
959
964
  }
960
965
 
961
966
  try {
962
- interface ThreadSummary {
963
- summary: { total_messages: number };
964
- }
965
- const summary = await mcpCall<ThreadSummary>("summarize_thread", {
966
- project_key: projectKey,
967
- thread_id: threadId,
968
- llm_mode: false, // Just need the count
967
+ // Use embedded swarm-mail inbox to count messages in thread
968
+ const inbox = await getSwarmInbox({
969
+ projectPath: projectKey,
970
+ agentName: "coordinator", // Dummy agent name for thread query
971
+ limit: 5,
972
+ includeBodies: false,
969
973
  });
970
- return summary.summary.total_messages;
974
+
975
+ // Count messages that match the thread ID
976
+ const threadMessages = inbox.messages.filter(
977
+ (m) => m.thread_id === threadId,
978
+ );
979
+ return threadMessages.length;
971
980
  } catch (error) {
972
981
  // Thread might not exist yet, or query failed
973
982
  console.warn(
@@ -1312,9 +1321,10 @@ export const swarm_plan_prompt = tool({
1312
1321
 
1313
1322
  // Fetch skills context
1314
1323
  let skillsContext = "";
1315
- let skillsInfo: { included: boolean; count?: number; relevant?: string[] } = {
1316
- included: false,
1317
- };
1324
+ let skillsInfo: { included: boolean; count?: number; relevant?: string[] } =
1325
+ {
1326
+ included: false,
1327
+ };
1318
1328
 
1319
1329
  if (args.include_skills !== false) {
1320
1330
  const allSkills = await listSkills();
@@ -1769,15 +1779,14 @@ export const swarm_progress = tool({
1769
1779
  ? args.bead_id.split(".")[0]
1770
1780
  : args.bead_id;
1771
1781
 
1772
- // Send progress message to thread (with auto-reinit on server restart)
1773
- await mcpCallWithAutoInit("send_message", {
1774
- project_key: args.project_key,
1775
- agent_name: args.agent_name,
1776
- sender_name: args.agent_name,
1777
- to: [], // Coordinator will pick it up from thread
1782
+ // Send progress message to thread using embedded swarm-mail
1783
+ await sendSwarmMessage({
1784
+ projectPath: args.project_key,
1785
+ fromAgent: args.agent_name,
1786
+ toAgents: [], // Coordinator will pick it up from thread
1778
1787
  subject: `Progress: ${args.bead_id} - ${args.status}`,
1779
- body_md: formatProgressMessage(validated),
1780
- thread_id: epicId,
1788
+ body: formatProgressMessage(validated),
1789
+ threadId: epicId,
1781
1790
  importance: args.status === "blocked" ? "high" : "normal",
1782
1791
  });
1783
1792
 
@@ -1895,6 +1904,12 @@ export const swarm_broadcast = tool({
1895
1904
  description:
1896
1905
  "Broadcast context update to all agents working on the same epic",
1897
1906
  args: {
1907
+ project_path: tool.schema
1908
+ .string()
1909
+ .describe("Absolute path to project root"),
1910
+ agent_name: tool.schema
1911
+ .string()
1912
+ .describe("Name of the agent broadcasting the message"),
1898
1913
  epic_id: tool.schema.string().describe("Epic ID (e.g., bd-abc123)"),
1899
1914
  message: tool.schema
1900
1915
  .string()
@@ -1909,18 +1924,14 @@ export const swarm_broadcast = tool({
1909
1924
  .describe("Files this context relates to"),
1910
1925
  },
1911
1926
  async execute(args, ctx) {
1912
- // Get agent state - requires prior initialization
1913
- const state = requireState(ctx.sessionID);
1914
-
1915
1927
  // Extract bead_id from context if available (for traceability)
1916
- // In the swarm flow, ctx might have the current bead being worked on
1917
1928
  const beadId = (ctx as { beadId?: string }).beadId || "unknown";
1918
1929
 
1919
1930
  // Format the broadcast message
1920
1931
  const body = [
1921
1932
  `## Context Update`,
1922
1933
  "",
1923
- `**From**: ${state.agentName} (${beadId})`,
1934
+ `**From**: ${args.agent_name} (${beadId})`,
1924
1935
  `**Priority**: ${args.importance.toUpperCase()}`,
1925
1936
  "",
1926
1937
  args.message,
@@ -1940,25 +1951,23 @@ export const swarm_broadcast = tool({
1940
1951
  ? "high"
1941
1952
  : "normal";
1942
1953
 
1943
- // Send as broadcast to thread (empty 'to' = all agents in thread)
1944
- // Uses auto-reinit wrapper to handle server restarts gracefully
1945
- await mcpCallWithAutoInit("send_message", {
1946
- project_key: state.projectKey,
1947
- agent_name: state.agentName,
1948
- sender_name: state.agentName,
1949
- to: [], // Broadcast to thread
1950
- subject: `[${args.importance.toUpperCase()}] Context update from ${state.agentName}`,
1951
- body_md: body,
1952
- thread_id: args.epic_id,
1954
+ // Send as broadcast to thread using embedded swarm-mail
1955
+ await sendSwarmMessage({
1956
+ projectPath: args.project_path,
1957
+ fromAgent: args.agent_name,
1958
+ toAgents: [], // Broadcast to thread
1959
+ subject: `[${args.importance.toUpperCase()}] Context update from ${args.agent_name}`,
1960
+ body,
1961
+ threadId: args.epic_id,
1953
1962
  importance: mailImportance,
1954
- ack_required: args.importance === "blocker", // Require ack for blockers
1963
+ ackRequired: args.importance === "blocker",
1955
1964
  });
1956
1965
 
1957
1966
  return JSON.stringify(
1958
1967
  {
1959
1968
  broadcast: true,
1960
1969
  epic_id: args.epic_id,
1961
- from: state.agentName,
1970
+ from: args.agent_name,
1962
1971
  bead_id: beadId,
1963
1972
  importance: args.importance,
1964
1973
  recipients: "all agents in epic",
@@ -2070,16 +2079,15 @@ export const swarm_complete = tool({
2070
2079
  );
2071
2080
  }
2072
2081
 
2073
- // Release file reservations for this agent
2074
- // Uses auto-reinit wrapper to handle server restarts - this was the original
2075
- // failure point that prompted the self-healing implementation
2082
+ // Release file reservations for this agent using embedded swarm-mail
2076
2083
  try {
2077
- await mcpCallWithAutoInit("release_file_reservations", {
2078
- project_key: args.project_key,
2079
- agent_name: args.agent_name,
2084
+ await releaseSwarmFiles({
2085
+ projectPath: args.project_key,
2086
+ agentName: args.agent_name,
2087
+ // Release all reservations for this agent
2080
2088
  });
2081
2089
  } catch (error) {
2082
- // Even with auto-reinit, release might fail (e.g., no reservations existed)
2090
+ // Release might fail (e.g., no reservations existed)
2083
2091
  // This is non-fatal - log and continue
2084
2092
  console.warn(
2085
2093
  `[swarm] Failed to release file reservations for ${args.agent_name}:`,
@@ -2092,7 +2100,7 @@ export const swarm_complete = tool({
2092
2100
  ? args.bead_id.split(".")[0]
2093
2101
  : args.bead_id;
2094
2102
 
2095
- // Send completion message
2103
+ // Send completion message using embedded swarm-mail
2096
2104
  const completionBody = [
2097
2105
  `## Subtask Complete: ${args.bead_id}`,
2098
2106
  "",
@@ -2108,14 +2116,13 @@ export const swarm_complete = tool({
2108
2116
  .filter(Boolean)
2109
2117
  .join("\n");
2110
2118
 
2111
- await mcpCallWithAutoInit("send_message", {
2112
- project_key: args.project_key,
2113
- agent_name: args.agent_name,
2114
- sender_name: args.agent_name,
2115
- to: [], // Thread broadcast
2119
+ await sendSwarmMessage({
2120
+ projectPath: args.project_key,
2121
+ fromAgent: args.agent_name,
2122
+ toAgents: [], // Thread broadcast
2116
2123
  subject: `Complete: ${args.bead_id}`,
2117
- body_md: completionBody,
2118
- thread_id: epicId,
2124
+ body: completionBody,
2125
+ threadId: epicId,
2119
2126
  importance: "normal",
2120
2127
  });
2121
2128
 
@@ -2650,7 +2657,14 @@ This tool helps you formalize learnings into a skill that future agents can disc
2650
2657
  .string()
2651
2658
  .describe("Brief summary of what was learned (1-2 sentences)"),
2652
2659
  pattern_type: tool.schema
2653
- .enum(["code-pattern", "best-practice", "gotcha", "tool-usage", "domain-knowledge", "workflow"])
2660
+ .enum([
2661
+ "code-pattern",
2662
+ "best-practice",
2663
+ "gotcha",
2664
+ "tool-usage",
2665
+ "domain-knowledge",
2666
+ "workflow",
2667
+ ])
2654
2668
  .describe("Category of the learning"),
2655
2669
  details: tool.schema
2656
2670
  .string()
@@ -2669,7 +2683,9 @@ This tool helps you formalize learnings into a skill that future agents can disc
2669
2683
  create_skill: tool.schema
2670
2684
  .boolean()
2671
2685
  .optional()
2672
- .describe("Create a skill from this learning (default: false, just document)"),
2686
+ .describe(
2687
+ "Create a skill from this learning (default: false, just document)",
2688
+ ),
2673
2689
  skill_name: tool.schema
2674
2690
  .string()
2675
2691
  .regex(/^[a-z0-9-]+$/)
@@ -2737,7 +2753,8 @@ ${args.files_context && args.files_context.length > 0 ? `## Reference Files\n\n$
2737
2753
  error: `Skill '${args.skill_name}' already exists`,
2738
2754
  existing_path: existing.path,
2739
2755
  learning: learning,
2740
- suggestion: "Use skills_update to add to existing skill, or choose a different name",
2756
+ suggestion:
2757
+ "Use skills_update to add to existing skill, or choose a different name",
2741
2758
  },
2742
2759
  null,
2743
2760
  2,
@@ -2745,7 +2762,12 @@ ${args.files_context && args.files_context.length > 0 ? `## Reference Files\n\n$
2745
2762
  }
2746
2763
 
2747
2764
  // Create skill directory and file
2748
- const skillDir = join(process.cwd(), ".opencode", "skills", args.skill_name);
2765
+ const skillDir = join(
2766
+ process.cwd(),
2767
+ ".opencode",
2768
+ "skills",
2769
+ args.skill_name,
2770
+ );
2749
2771
  const skillPath = join(skillDir, "SKILL.md");
2750
2772
 
2751
2773
  const frontmatter = [
@@ -2798,8 +2820,10 @@ ${args.files_context && args.files_context.length > 0 ? `## Reference Files\n\n$
2798
2820
  success: true,
2799
2821
  skill_created: false,
2800
2822
  learning: learning,
2801
- message: "Learning documented. Use create_skill=true to persist as a skill for future agents.",
2802
- suggested_skill_name: args.skill_name ||
2823
+ message:
2824
+ "Learning documented. Use create_skill=true to persist as a skill for future agents.",
2825
+ suggested_skill_name:
2826
+ args.skill_name ||
2803
2827
  args.summary
2804
2828
  .toLowerCase()
2805
2829
  .replace(/[^a-z0-9\s-]/g, "")
@@ -3029,7 +3053,8 @@ export const swarm_init = tool({
3029
3053
  if (availableSkills.length > 0) {
3030
3054
  skillsGuidance = `Found ${availableSkills.length} skill(s). Use skills_list to see details, skills_use to activate.`;
3031
3055
  } else {
3032
- skillsGuidance = "No skills found. Add skills to .opencode/skills/ or .claude/skills/ for specialized guidance.";
3056
+ skillsGuidance =
3057
+ "No skills found. Add skills to .opencode/skills/ or .claude/skills/ for specialized guidance.";
3033
3058
  }
3034
3059
 
3035
3060
  return JSON.stringify(
@@ -9,14 +9,18 @@
9
9
  * - cass: Cross-agent session search for historical context
10
10
  * - ubs: Universal bug scanner for pre-commit checks
11
11
  * - beads (bd): Git-backed issue tracking
12
- * - agent-mail: Multi-agent coordination server
12
+ * - swarm-mail: Embedded multi-agent coordination (PGLite-based)
13
+ * - agent-mail: DEPRECATED - Legacy MCP server (use swarm-mail instead)
13
14
  */
14
15
 
16
+ import { checkSwarmHealth } from "./streams/swarm-mail";
17
+
15
18
  export type ToolName =
16
19
  | "semantic-memory"
17
20
  | "cass"
18
21
  | "ubs"
19
22
  | "beads"
23
+ | "swarm-mail"
20
24
  | "agent-mail";
21
25
 
22
26
  export interface ToolStatus {
@@ -199,6 +203,32 @@ const toolCheckers: Record<ToolName, () => Promise<ToolStatus>> = {
199
203
  }
200
204
  },
201
205
 
206
+ "swarm-mail": async () => {
207
+ try {
208
+ // Note: checkSwarmHealth() accepts optional projectPath parameter.
209
+ // For tool availability checking, we call it without args to check global health.
210
+ // This is intentional - we're verifying the embedded Swarm Mail system is functional,
211
+ // not checking health for a specific project.
212
+ const healthResult = await checkSwarmHealth();
213
+ return {
214
+ available: healthResult.healthy,
215
+ checkedAt: new Date().toISOString(),
216
+ error: healthResult.healthy
217
+ ? undefined
218
+ : "Swarm Mail database not healthy",
219
+ version: "embedded",
220
+ };
221
+ } catch (e) {
222
+ return {
223
+ available: false,
224
+ checkedAt: new Date().toISOString(),
225
+ error: String(e),
226
+ };
227
+ }
228
+ },
229
+
230
+ // DEPRECATED: Use swarm-mail instead
231
+ // Kept for backward compatibility only
202
232
  "agent-mail": async () => {
203
233
  const reachable = await urlReachable(
204
234
  "http://127.0.0.1:8765/health/liveness",
@@ -220,8 +250,10 @@ const fallbackBehaviors: Record<ToolName, string> = {
220
250
  cass: "Decomposition proceeds without historical context from past sessions",
221
251
  ubs: "Subtask completion skips bug scanning - manual review recommended",
222
252
  beads: "Swarm cannot track issues - task coordination will be less reliable",
223
- "agent-mail":
253
+ "swarm-mail":
224
254
  "Multi-agent coordination disabled - file conflicts possible if multiple agents active",
255
+ "agent-mail":
256
+ "DEPRECATED: Use swarm-mail instead. Legacy MCP server mode - file conflicts possible if multiple agents active",
225
257
  };
226
258
 
227
259
  /**
@@ -276,6 +308,7 @@ export async function checkAllTools(): Promise<
276
308
  "cass",
277
309
  "ubs",
278
310
  "beads",
311
+ "swarm-mail",
279
312
  "agent-mail",
280
313
  ];
281
314