opencode-swarm-plugin 0.3.0 → 0.5.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.
@@ -16,6 +16,8 @@ import {
16
16
  swarm_complete,
17
17
  swarm_subtask_prompt,
18
18
  swarm_evaluation_prompt,
19
+ formatSubtaskPromptV2,
20
+ SUBTASK_PROMPT_V2,
19
21
  } from "./swarm";
20
22
  import { mcpCall, setState, clearState, AGENT_MAIL_URL } from "./agent-mail";
21
23
 
@@ -952,3 +954,116 @@ describe("Graceful Degradation", () => {
952
954
  expect(result).toContain("Report progress");
953
955
  });
954
956
  });
957
+
958
+ // ============================================================================
959
+ // Coordinator-Centric Swarm Tools (V2)
960
+ // ============================================================================
961
+
962
+ describe("Swarm Prompt V2 (with Agent Mail/Beads)", () => {
963
+ describe("formatSubtaskPromptV2", () => {
964
+ it("generates correct prompt with all fields", () => {
965
+ const result = formatSubtaskPromptV2({
966
+ bead_id: "bd-123.1",
967
+ epic_id: "bd-123",
968
+ subtask_title: "Add OAuth provider",
969
+ subtask_description: "Configure Google OAuth in the auth config",
970
+ files: ["src/auth/google.ts", "src/auth/config.ts"],
971
+ shared_context: "We are using NextAuth.js v5",
972
+ });
973
+
974
+ // Check title is included
975
+ expect(result).toContain("Add OAuth provider");
976
+
977
+ // Check description is included
978
+ expect(result).toContain("Configure Google OAuth in the auth config");
979
+
980
+ // Check files are formatted as list
981
+ expect(result).toContain("- `src/auth/google.ts`");
982
+ expect(result).toContain("- `src/auth/config.ts`");
983
+
984
+ // Check shared context is included
985
+ expect(result).toContain("We are using NextAuth.js v5");
986
+
987
+ // Check bead/epic IDs are substituted
988
+ expect(result).toContain("bd-123.1");
989
+ expect(result).toContain("bd-123");
990
+ });
991
+
992
+ it("handles missing optional fields", () => {
993
+ const result = formatSubtaskPromptV2({
994
+ bead_id: "bd-456.1",
995
+ epic_id: "bd-456",
996
+ subtask_title: "Simple task",
997
+ subtask_description: "",
998
+ files: [],
999
+ });
1000
+
1001
+ // Check title is included
1002
+ expect(result).toContain("Simple task");
1003
+
1004
+ // Check fallback for empty description
1005
+ expect(result).toContain("(see title)");
1006
+
1007
+ // Check fallback for empty files
1008
+ expect(result).toContain("(no specific files - use judgment)");
1009
+
1010
+ // Check fallback for missing context
1011
+ expect(result).toContain("(none)");
1012
+ });
1013
+
1014
+ it("handles files with special characters", () => {
1015
+ const result = formatSubtaskPromptV2({
1016
+ bead_id: "bd-789.1",
1017
+ epic_id: "bd-789",
1018
+ subtask_title: "Handle paths",
1019
+ subtask_description: "Test file paths",
1020
+ files: [
1021
+ "src/components/[slug]/page.tsx",
1022
+ "src/api/users/[id]/route.ts",
1023
+ ],
1024
+ });
1025
+
1026
+ expect(result).toContain("- `src/components/[slug]/page.tsx`");
1027
+ expect(result).toContain("- `src/api/users/[id]/route.ts`");
1028
+ });
1029
+ });
1030
+
1031
+ describe("SUBTASK_PROMPT_V2", () => {
1032
+ it("contains expected sections", () => {
1033
+ // Check all main sections are present in the template
1034
+ expect(SUBTASK_PROMPT_V2).toContain("## Task");
1035
+ expect(SUBTASK_PROMPT_V2).toContain("{subtask_title}");
1036
+ expect(SUBTASK_PROMPT_V2).toContain("{subtask_description}");
1037
+
1038
+ expect(SUBTASK_PROMPT_V2).toContain("## Files");
1039
+ expect(SUBTASK_PROMPT_V2).toContain("{file_list}");
1040
+
1041
+ expect(SUBTASK_PROMPT_V2).toContain("## Context");
1042
+ expect(SUBTASK_PROMPT_V2).toContain("{shared_context}");
1043
+
1044
+ expect(SUBTASK_PROMPT_V2).toContain("## Workflow");
1045
+ });
1046
+
1047
+ it("DOES contain Agent Mail instructions", () => {
1048
+ // V2 prompt tells agents to USE Agent Mail
1049
+ expect(SUBTASK_PROMPT_V2).toContain("Agent Mail");
1050
+ expect(SUBTASK_PROMPT_V2).toContain("agentmail_send");
1051
+ expect(SUBTASK_PROMPT_V2).toContain("thread_id");
1052
+ });
1053
+
1054
+ it("DOES contain beads instructions", () => {
1055
+ // V2 prompt tells agents to USE beads
1056
+ expect(SUBTASK_PROMPT_V2).toContain("{bead_id}");
1057
+ expect(SUBTASK_PROMPT_V2).toContain("{epic_id}");
1058
+ expect(SUBTASK_PROMPT_V2).toContain("beads_update");
1059
+ expect(SUBTASK_PROMPT_V2).toContain("beads_create");
1060
+ expect(SUBTASK_PROMPT_V2).toContain("swarm_complete");
1061
+ });
1062
+
1063
+ it("instructs agents to communicate", () => {
1064
+ expect(SUBTASK_PROMPT_V2).toContain("Never work silently");
1065
+ expect(SUBTASK_PROMPT_V2).toContain("Report progress");
1066
+ expect(SUBTASK_PROMPT_V2).toContain("coordinator");
1067
+ });
1068
+ });
1069
+ });
package/src/swarm.ts CHANGED
@@ -359,6 +359,87 @@ Before writing code:
359
359
 
360
360
  Begin work on your subtask now.`;
361
361
 
362
+ /**
363
+ * Streamlined subtask prompt (V2) - still uses Agent Mail and beads
364
+ *
365
+ * This is a cleaner version of SUBTASK_PROMPT that's easier to parse.
366
+ * Agents MUST use Agent Mail for communication and beads for tracking.
367
+ */
368
+ export const SUBTASK_PROMPT_V2 = `You are a swarm agent working on: **{subtask_title}**
369
+
370
+ ## Identity
371
+ - **Bead ID**: {bead_id}
372
+ - **Epic ID**: {epic_id}
373
+
374
+ ## Task
375
+ {subtask_description}
376
+
377
+ ## Files (exclusive reservation)
378
+ {file_list}
379
+
380
+ Only modify these files. Need others? Message the coordinator.
381
+
382
+ ## Context
383
+ {shared_context}
384
+
385
+ ## MANDATORY: Use These Tools
386
+
387
+ ### Agent Mail - communicate with the swarm
388
+ \`\`\`typescript
389
+ // Report progress, ask questions, announce blockers
390
+ agentmail_send({
391
+ to: ["coordinator"],
392
+ subject: "Progress update",
393
+ body: "What you did or need",
394
+ thread_id: "{epic_id}"
395
+ })
396
+ \`\`\`
397
+
398
+ ### Beads - track your work
399
+ - **Blocked?** \`beads_update({ id: "{bead_id}", status: "blocked" })\`
400
+ - **Found bug?** \`beads_create({ title: "Bug description", type: "bug" })\`
401
+ - **Done?** \`swarm_complete({ bead_id: "{bead_id}", summary: "What you did", files_touched: [...] })\`
402
+
403
+ ## Workflow
404
+
405
+ 1. **Read** the files first
406
+ 2. **Plan** your approach (message coordinator if complex)
407
+ 3. **Implement** the changes
408
+ 4. **Verify** (typecheck, tests)
409
+ 5. **Report** progress via Agent Mail
410
+ 6. **Complete** with swarm_complete when done
411
+
412
+ **Never work silently.** Communicate progress and blockers immediately.
413
+
414
+ Begin now.`;
415
+
416
+ /**
417
+ * Format the V2 subtask prompt for a specific agent
418
+ */
419
+ export function formatSubtaskPromptV2(params: {
420
+ bead_id: string;
421
+ epic_id: string;
422
+ subtask_title: string;
423
+ subtask_description: string;
424
+ files: string[];
425
+ shared_context?: string;
426
+ }): string {
427
+ const fileList =
428
+ params.files.length > 0
429
+ ? params.files.map((f) => `- \`${f}\``).join("\n")
430
+ : "(no specific files - use judgment)";
431
+
432
+ return SUBTASK_PROMPT_V2.replace(/{bead_id}/g, params.bead_id)
433
+ .replace(/{epic_id}/g, params.epic_id)
434
+ .replace("{subtask_title}", params.subtask_title)
435
+ .replace(
436
+ "{subtask_description}",
437
+ params.subtask_description || "(see title)",
438
+ )
439
+ .replace("{file_list}", fileList)
440
+ .replace("{shared_context}", params.shared_context || "(none)");
441
+ }
442
+
362
443
  /**
363
444
  * Prompt for self-evaluation before completing a subtask.
364
445
  *
@@ -1418,6 +1499,199 @@ export const swarm_subtask_prompt = tool({
1418
1499
  },
1419
1500
  });
1420
1501
 
1502
+ /**
1503
+ * Prepare a subtask for spawning with Task tool (V2 prompt)
1504
+ *
1505
+ * Generates a streamlined prompt that tells agents to USE Agent Mail and beads.
1506
+ * Returns JSON that can be directly used with Task tool.
1507
+ */
1508
+ export const swarm_spawn_subtask = tool({
1509
+ description:
1510
+ "Prepare a subtask for spawning. Returns prompt with Agent Mail/beads instructions.",
1511
+ args: {
1512
+ bead_id: tool.schema.string().describe("Subtask bead ID"),
1513
+ epic_id: tool.schema.string().describe("Parent epic bead ID"),
1514
+ subtask_title: tool.schema.string().describe("Subtask title"),
1515
+ subtask_description: tool.schema
1516
+ .string()
1517
+ .optional()
1518
+ .describe("Detailed subtask instructions"),
1519
+ files: tool.schema
1520
+ .array(tool.schema.string())
1521
+ .describe("Files assigned to this subtask"),
1522
+ shared_context: tool.schema
1523
+ .string()
1524
+ .optional()
1525
+ .describe("Context shared across all agents"),
1526
+ },
1527
+ async execute(args) {
1528
+ const prompt = formatSubtaskPromptV2({
1529
+ bead_id: args.bead_id,
1530
+ epic_id: args.epic_id,
1531
+ subtask_title: args.subtask_title,
1532
+ subtask_description: args.subtask_description || "",
1533
+ files: args.files,
1534
+ shared_context: args.shared_context,
1535
+ });
1536
+
1537
+ return JSON.stringify(
1538
+ {
1539
+ prompt,
1540
+ bead_id: args.bead_id,
1541
+ epic_id: args.epic_id,
1542
+ files: args.files,
1543
+ },
1544
+ null,
1545
+ 2,
1546
+ );
1547
+ },
1548
+ });
1549
+
1550
+ /**
1551
+ * Schema for task agent result
1552
+ */
1553
+ const TaskResultSchema = z.object({
1554
+ success: z.boolean(),
1555
+ summary: z.string(),
1556
+ files_modified: z.array(z.string()).optional().default([]),
1557
+ files_created: z.array(z.string()).optional().default([]),
1558
+ issues_found: z.array(z.string()).optional().default([]),
1559
+ tests_passed: z.boolean().optional(),
1560
+ notes: z.string().optional(),
1561
+ blocker: z.string().optional(),
1562
+ suggestions: z.array(z.string()).optional(),
1563
+ });
1564
+
1565
+ type TaskResult = z.infer<typeof TaskResultSchema>;
1566
+
1567
+ /**
1568
+ * Handle subtask completion from a Task agent
1569
+ *
1570
+ * This tool is for coordinators to process the result after a Task subagent
1571
+ * returns. It parses the JSON result, closes the bead on success, and
1572
+ * creates new beads for any issues discovered.
1573
+ *
1574
+ * @example
1575
+ * // Task agent returns JSON:
1576
+ * // { "success": true, "summary": "Added auth", "files_modified": ["src/auth.ts"], "issues_found": ["Missing tests"] }
1577
+ * //
1578
+ * // Coordinator calls:
1579
+ * swarm_complete_subtask(bead_id="bd-123.1", task_result=<agent_response>)
1580
+ */
1581
+ export const swarm_complete_subtask = tool({
1582
+ description:
1583
+ "Handle subtask completion after Task agent returns. Parses result JSON, closes bead on success, creates new beads for issues found.",
1584
+ args: {
1585
+ bead_id: z.string().describe("Subtask bead ID to close"),
1586
+ task_result: z
1587
+ .string()
1588
+ .describe("JSON result from the Task agent (TaskResult schema)"),
1589
+ files_touched: z
1590
+ .array(z.string())
1591
+ .optional()
1592
+ .describe(
1593
+ "Override files touched (uses task_result.files_modified if not provided)",
1594
+ ),
1595
+ },
1596
+ async execute(args) {
1597
+ // Parse the task result JSON
1598
+ let result: TaskResult;
1599
+ try {
1600
+ const parsed = JSON.parse(args.task_result);
1601
+ result = TaskResultSchema.parse(parsed);
1602
+ } catch (error) {
1603
+ // Handle parse errors gracefully
1604
+ const errorMessage =
1605
+ error instanceof SyntaxError
1606
+ ? `Invalid JSON: ${error.message}`
1607
+ : error instanceof z.ZodError
1608
+ ? `Schema validation failed: ${error.issues.map((i) => i.message).join(", ")}`
1609
+ : String(error);
1610
+
1611
+ return JSON.stringify(
1612
+ {
1613
+ success: false,
1614
+ error: "Failed to parse task result",
1615
+ details: errorMessage,
1616
+ hint: "Task agent should return JSON matching TaskResult schema: { success, summary, files_modified?, issues_found?, ... }",
1617
+ },
1618
+ null,
1619
+ 2,
1620
+ );
1621
+ }
1622
+
1623
+ const filesTouched = args.files_touched ?? [
1624
+ ...result.files_modified,
1625
+ ...result.files_created,
1626
+ ];
1627
+ const issuesCreated: Array<{ title: string; id?: string }> = [];
1628
+
1629
+ // If task failed, don't close the bead - return info for coordinator to handle
1630
+ if (!result.success) {
1631
+ return JSON.stringify(
1632
+ {
1633
+ success: false,
1634
+ bead_id: args.bead_id,
1635
+ task_failed: true,
1636
+ summary: result.summary,
1637
+ blocker: result.blocker,
1638
+ suggestions: result.suggestions,
1639
+ files_touched: filesTouched,
1640
+ action_needed:
1641
+ "Task failed - review blocker and decide whether to retry or close as failed",
1642
+ },
1643
+ null,
1644
+ 2,
1645
+ );
1646
+ }
1647
+
1648
+ // Task succeeded - close the bead
1649
+ const closeReason = result.summary.slice(0, 200); // Truncate for safety
1650
+ await Bun.$`bd close ${args.bead_id} -r "${closeReason}"`.quiet().nothrow();
1651
+
1652
+ // Create new beads for each issue found
1653
+ if (result.issues_found.length > 0) {
1654
+ for (const issue of result.issues_found) {
1655
+ const issueTitle = issue.slice(0, 100); // Truncate long titles
1656
+ const createResult = await Bun.$`bd create "${issueTitle}" -t bug`
1657
+ .quiet()
1658
+ .nothrow();
1659
+
1660
+ if (createResult.exitCode === 0) {
1661
+ // Try to parse the bead ID from output
1662
+ const output = createResult.stdout.toString();
1663
+ const idMatch = output.match(/bd-[a-z0-9]+/);
1664
+ issuesCreated.push({
1665
+ title: issueTitle,
1666
+ id: idMatch?.[0],
1667
+ });
1668
+ } else {
1669
+ issuesCreated.push({
1670
+ title: issueTitle,
1671
+ id: undefined, // Failed to create
1672
+ });
1673
+ }
1674
+ }
1675
+ }
1676
+
1677
+ return JSON.stringify(
1678
+ {
1679
+ success: true,
1680
+ bead_id: args.bead_id,
1681
+ bead_closed: true,
1682
+ summary: result.summary,
1683
+ files_touched: filesTouched,
1684
+ tests_passed: result.tests_passed,
1685
+ notes: result.notes,
1686
+ issues_created: issuesCreated.length > 0 ? issuesCreated : undefined,
1687
+ issues_count: issuesCreated.length,
1688
+ },
1689
+ null,
1690
+ 2,
1691
+ );
1692
+ },
1693
+ });
1694
+
1421
1695
  /**
1422
1696
  * Generate self-evaluation prompt
1423
1697
  */
@@ -1560,5 +1834,7 @@ export const swarmTools = {
1560
1834
  swarm_complete: swarm_complete,
1561
1835
  swarm_record_outcome: swarm_record_outcome,
1562
1836
  swarm_subtask_prompt: swarm_subtask_prompt,
1837
+ swarm_spawn_subtask: swarm_spawn_subtask,
1838
+ swarm_complete_subtask: swarm_complete_subtask,
1563
1839
  swarm_evaluation_prompt: swarm_evaluation_prompt,
1564
1840
  };