opencode-swarm-plugin 0.28.1 → 0.29.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.
@@ -521,7 +521,7 @@ describe("beads integration", () => {
521
521
  it("links a bead to an Agent Mail thread", async () => {
522
522
  const threadId = "test-thread-123";
523
523
  const result = await beads_link_thread.execute(
524
- { cell_id: testBeadId, thread_id: threadId },
524
+ { bead_id: testBeadId, thread_id: threadId },
525
525
  mockContext,
526
526
  );
527
527
 
@@ -540,13 +540,13 @@ describe("beads integration", () => {
540
540
 
541
541
  // Link once
542
542
  await beads_link_thread.execute(
543
- { cell_id: testBeadId, thread_id: threadId },
543
+ { bead_id: testBeadId, thread_id: threadId },
544
544
  mockContext,
545
545
  );
546
546
 
547
547
  // Try to link again
548
548
  const result = await beads_link_thread.execute(
549
- { cell_id: testBeadId, thread_id: threadId },
549
+ { bead_id: testBeadId, thread_id: threadId },
550
550
  mockContext,
551
551
  );
552
552
 
@@ -562,7 +562,7 @@ describe("beads integration", () => {
562
562
 
563
563
  const threadId = "test-thread-789";
564
564
  await beads_link_thread.execute(
565
- { cell_id: testBeadId, thread_id: threadId },
565
+ { bead_id: testBeadId, thread_id: threadId },
566
566
  mockContext,
567
567
  );
568
568
 
@@ -576,7 +576,7 @@ describe("beads integration", () => {
576
576
  it("throws BeadError for invalid bead ID", async () => {
577
577
  await expect(
578
578
  beads_link_thread.execute(
579
- { cell_id: "nonexistent-bead-xyz", thread_id: "thread-123" },
579
+ { bead_id: "nonexistent-bead-xyz", thread_id: "thread-123" },
580
580
  mockContext,
581
581
  ),
582
582
  ).rejects.toThrow(BeadError);
package/src/hive.ts CHANGED
@@ -1094,7 +1094,7 @@ export const hive_sync = tool({
1094
1094
  export const hive_link_thread = tool({
1095
1095
  description: "Add metadata linking cell to Agent Mail thread",
1096
1096
  args: {
1097
- cell_id: tool.schema.string().describe("Cell ID"),
1097
+ bead_id: tool.schema.string().describe("Cell ID"),
1098
1098
  thread_id: tool.schema.string().describe("Agent Mail thread ID"),
1099
1099
  },
1100
1100
  async execute(args, ctx) {
@@ -1102,11 +1102,11 @@ export const hive_link_thread = tool({
1102
1102
  const adapter = await getHiveAdapter(projectKey);
1103
1103
 
1104
1104
  try {
1105
- const cell = await adapter.getCell(projectKey, args.cell_id);
1105
+ const cell = await adapter.getCell(projectKey, args.bead_id);
1106
1106
 
1107
1107
  if (!cell) {
1108
1108
  throw new HiveError(
1109
- `Cell not found: ${args.cell_id}`,
1109
+ `Cell not found: ${args.bead_id}`,
1110
1110
  "hive_link_thread",
1111
1111
  );
1112
1112
  }
@@ -1115,20 +1115,20 @@ export const hive_link_thread = tool({
1115
1115
  const threadMarker = `[thread:${args.thread_id}]`;
1116
1116
 
1117
1117
  if (existingDesc.includes(threadMarker)) {
1118
- return `Cell ${args.cell_id} already linked to thread ${args.thread_id}`;
1118
+ return `Cell ${args.bead_id} already linked to thread ${args.thread_id}`;
1119
1119
  }
1120
1120
 
1121
1121
  const newDesc = existingDesc
1122
1122
  ? `${existingDesc}\n\n${threadMarker}`
1123
1123
  : threadMarker;
1124
1124
 
1125
- await adapter.updateCell(projectKey, args.cell_id, {
1125
+ await adapter.updateCell(projectKey, args.bead_id, {
1126
1126
  description: newDesc,
1127
1127
  });
1128
1128
 
1129
- await adapter.markDirty(projectKey, args.cell_id);
1129
+ await adapter.markDirty(projectKey, args.bead_id);
1130
1130
 
1131
- return `Linked cell ${args.cell_id} to thread ${args.thread_id}`;
1131
+ return `Linked cell ${args.bead_id} to thread ${args.thread_id}`;
1132
1132
  } catch (error) {
1133
1133
  const message = error instanceof Error ? error.message : String(error);
1134
1134
  throw new HiveError(
@@ -1572,7 +1572,13 @@ describe("3-Strike Detection", () => {
1572
1572
  "Failed 1",
1573
1573
  storage,
1574
1574
  );
1575
- await new Promise((resolve) => setTimeout(resolve, 100));
1575
+ // Capture the timestamps from first strike
1576
+ const firstStrikeAt = record1.first_strike_at;
1577
+ const firstLastStrikeAt = record1.last_strike_at;
1578
+
1579
+ // Wait to ensure different timestamp
1580
+ await new Promise((resolve) => setTimeout(resolve, 10));
1581
+
1576
1582
  const record2 = await addStrike(
1577
1583
  "test-bead-4",
1578
1584
  "Fix 2",
@@ -1580,8 +1586,10 @@ describe("3-Strike Detection", () => {
1580
1586
  storage,
1581
1587
  );
1582
1588
 
1583
- expect(record2.first_strike_at).toBe(record1.first_strike_at);
1584
- expect(record2.last_strike_at).not.toBe(record1.last_strike_at);
1589
+ // first_strike_at should be preserved from first call
1590
+ expect(record2.first_strike_at).toBe(firstStrikeAt);
1591
+ // last_strike_at should be updated (different from first call's last_strike_at)
1592
+ expect(record2.last_strike_at).not.toBe(firstLastStrikeAt);
1585
1593
  });
1586
1594
  });
1587
1595
 
@@ -67,6 +67,7 @@ import {
67
67
  isToolAvailable,
68
68
  warnMissingTool,
69
69
  } from "./tool-availability";
70
+ import { getHiveAdapter } from "./hive";
70
71
  import { listSkills } from "./skills";
71
72
  import {
72
73
  canUseWorktreeIsolation,
@@ -82,45 +83,34 @@ import {
82
83
  // ============================================================================
83
84
 
84
85
  /**
85
- * Query beads for subtasks of an epic
86
+ * Query beads for subtasks of an epic using HiveAdapter (not bd CLI)
86
87
  */
87
- async function queryEpicSubtasks(epicId: string): Promise<Bead[]> {
88
- // Check if beads is available
89
- const beadsAvailable = await isToolAvailable("beads");
90
- if (!beadsAvailable) {
91
- warnMissingTool("beads");
92
- return []; // Return empty - swarm can still function without status tracking
93
- }
94
-
95
- const result = await Bun.$`bd list --parent ${epicId} --json`
96
- .quiet()
97
- .nothrow();
98
-
99
- if (result.exitCode !== 0) {
100
- // Don't throw - just return empty and log error prominently
101
- console.error(
102
- `[swarm] ERROR: Failed to query subtasks for epic ${epicId}:`,
103
- result.stderr.toString(),
104
- );
105
- return [];
106
- }
107
-
88
+ async function queryEpicSubtasks(projectKey: string, epicId: string): Promise<Bead[]> {
108
89
  try {
109
- const parsed = JSON.parse(result.stdout.toString());
110
- return z.array(BeadSchema).parse(parsed);
90
+ const adapter = await getHiveAdapter(projectKey);
91
+ const cells = await adapter.queryCells(projectKey, { parent_id: epicId });
92
+ // Map Cell (from HiveAdapter) to Bead schema format
93
+ // Cell uses `type` and numeric timestamps, Bead uses `issue_type` and ISO strings
94
+ return cells
95
+ .filter(cell => cell.status !== "tombstone") // Exclude deleted cells
96
+ .map(cell => ({
97
+ id: cell.id,
98
+ title: cell.title,
99
+ description: cell.description || "",
100
+ status: cell.status as "open" | "in_progress" | "blocked" | "closed",
101
+ priority: cell.priority,
102
+ issue_type: cell.type as "bug" | "feature" | "task" | "epic" | "chore",
103
+ created_at: new Date(cell.created_at).toISOString(),
104
+ updated_at: cell.updated_at ? new Date(cell.updated_at).toISOString() : undefined,
105
+ dependencies: [], // Dependencies fetched separately if needed
106
+ metadata: {},
107
+ }));
111
108
  } catch (error) {
112
- if (error instanceof z.ZodError) {
113
- console.error(
114
- `[swarm] ERROR: Invalid bead data for epic ${epicId}:`,
115
- error.message,
116
- );
117
- return [];
118
- }
119
109
  console.error(
120
- `[swarm] ERROR: Failed to parse beads for epic ${epicId}:`,
121
- error,
110
+ `[swarm] ERROR: Failed to query subtasks for epic ${epicId}:`,
111
+ error instanceof Error ? error.message : String(error),
122
112
  );
123
- throw error;
113
+ return [];
124
114
  }
125
115
  }
126
116
 
@@ -758,7 +748,7 @@ export const swarm_status = tool({
758
748
  },
759
749
  async execute(args) {
760
750
  // Query subtasks from beads
761
- const subtasks = await queryEpicSubtasks(args.epic_id);
751
+ const subtasks = await queryEpicSubtasks(args.project_key, args.epic_id);
762
752
 
763
753
  // Count statuses
764
754
  const statusCounts = {
@@ -878,12 +868,16 @@ export const swarm_progress = tool({
878
868
  // Validate
879
869
  const validated = AgentProgressSchema.parse(progress);
880
870
 
881
- // Update cell status if needed
871
+ // Update cell status if needed (using HiveAdapter, not bd CLI)
882
872
  if (args.status === "blocked" || args.status === "in_progress") {
883
- const beadStatus = args.status === "blocked" ? "blocked" : "in_progress";
884
- await Bun.$`bd update ${args.bead_id} --status ${beadStatus} --json`
885
- .quiet()
886
- .nothrow();
873
+ try {
874
+ const adapter = await getHiveAdapter(args.project_key);
875
+ const newStatus = args.status === "blocked" ? "blocked" : "in_progress";
876
+ await adapter.changeCellStatus(args.project_key, args.bead_id, newStatus);
877
+ } catch (error) {
878
+ // Non-fatal - log but continue
879
+ console.error(`[swarm] Failed to update cell status: ${error instanceof Error ? error.message : String(error)}`);
880
+ }
887
881
  }
888
882
 
889
883
  // Extract epic ID from bead ID (e.g., bd-abc123.1 -> bd-abc123)
@@ -1152,11 +1146,35 @@ Or use skip_review=true to bypass (not recommended for production work).`,
1152
1146
  }
1153
1147
 
1154
1148
  try {
1155
- // Verify agent is registered in swarm-mail
1156
- // This catches agents who skipped swarmmail_init
1149
+ // Validate bead_id exists and is not already closed (EARLY validation)
1157
1150
  const projectKey = args.project_key
1158
1151
  .replace(/\//g, "-")
1159
1152
  .replace(/\\/g, "-");
1153
+
1154
+ // Use HiveAdapter for validation (not bd CLI)
1155
+ const adapter = await getHiveAdapter(args.project_key);
1156
+
1157
+ // 1. Check if bead exists
1158
+ const cell = await adapter.getCell(projectKey, args.bead_id);
1159
+ if (!cell) {
1160
+ return JSON.stringify({
1161
+ success: false,
1162
+ error: `Bead not found: ${args.bead_id}`,
1163
+ hint: "Check the bead ID is correct. Use hive_query to list open cells.",
1164
+ });
1165
+ }
1166
+
1167
+ // 2. Check if bead is already closed
1168
+ if (cell.status === "closed") {
1169
+ return JSON.stringify({
1170
+ success: false,
1171
+ error: `Bead already closed: ${args.bead_id}`,
1172
+ hint: "This bead was already completed. No action needed.",
1173
+ });
1174
+ }
1175
+
1176
+ // Verify agent is registered in swarm-mail
1177
+ // This catches agents who skipped swarmmail_init
1160
1178
  let agentRegistered = false;
1161
1179
  let registrationWarning = "";
1162
1180
 
@@ -1303,49 +1321,27 @@ Continuing with completion, but this should be fixed for future subtasks.`;
1303
1321
  }
1304
1322
  }
1305
1323
 
1306
- // Close the cell - use project_key as working directory to find correct .beads/
1307
- // This fixes the issue where cell ID prefix (e.g., "pdf-library-g84.2") doesn't match CWD
1308
- const closeResult =
1309
- await Bun.$`bd close ${args.bead_id} --reason ${args.summary} --json`
1310
- .cwd(args.project_key)
1311
- .quiet()
1312
- .nothrow();
1313
-
1314
- if (closeResult.exitCode !== 0) {
1315
- const stderrOutput = closeResult.stderr.toString().trim();
1316
- const stdoutOutput = closeResult.stdout.toString().trim();
1317
-
1318
- // Check for common error patterns and provide better guidance
1319
- const isNoDatabaseError = stderrOutput.includes("no beads database found");
1320
- const isNotFoundError = stderrOutput.includes("not found") || stderrOutput.includes("does not exist");
1321
-
1324
+ // Close the cell using HiveAdapter (not bd CLI)
1325
+ try {
1326
+ await adapter.closeCell(args.project_key, args.bead_id, args.summary);
1327
+ } catch (closeError) {
1328
+ const errorMessage = closeError instanceof Error ? closeError.message : String(closeError);
1322
1329
  return JSON.stringify(
1323
1330
  {
1324
1331
  success: false,
1325
1332
  error: "Failed to close cell",
1326
- failed_step: "bd close",
1327
- details: stderrOutput || stdoutOutput || "Unknown error from bd close command",
1333
+ failed_step: "closeCell",
1334
+ details: errorMessage,
1328
1335
  bead_id: args.bead_id,
1329
1336
  project_key: args.project_key,
1330
1337
  recovery: {
1331
- steps: isNoDatabaseError
1332
- ? [
1333
- `1. Verify project_key is correct: "${args.project_key}"`,
1334
- `2. Check .beads/ exists in that directory`,
1335
- `3. Cell ID prefix "${args.bead_id.split("-")[0]}" should match project`,
1336
- `4. Try: hive_close(id="${args.bead_id}", reason="...")`,
1337
- ]
1338
- : [
1339
- `1. Check cell exists: bd show ${args.bead_id}`,
1340
- `2. Check cell status (might already be closed): hive_query()`,
1341
- `3. If cell is blocked, unblock first: hive_update(id="${args.bead_id}", status="in_progress")`,
1342
- `4. Try closing directly: hive_close(id="${args.bead_id}", reason="...")`,
1343
- ],
1344
- hint: isNoDatabaseError
1345
- ? `The project_key "${args.project_key}" doesn't have a .beads/ directory. Make sure you're using the correct project path.`
1346
- : isNotFoundError
1347
- ? `Cell "${args.bead_id}" not found. It may have been closed already or the ID is incorrect.`
1348
- : "If cell is in 'blocked' status, you must change it to 'in_progress' or 'open' before closing.",
1338
+ steps: [
1339
+ `1. Check cell exists: hive_query()`,
1340
+ `2. Check cell status (might already be closed)`,
1341
+ `3. If cell is blocked, unblock first: hive_update(id="${args.bead_id}", status="in_progress")`,
1342
+ `4. Try closing directly: hive_close(id="${args.bead_id}", reason="...")`,
1343
+ ],
1344
+ hint: "Cell may already be closed, or the ID is incorrect.",
1349
1345
  },
1350
1346
  },
1351
1347
  null,
@@ -1645,12 +1641,13 @@ Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
1645
1641
  return JSON.stringify(
1646
1642
  {
1647
1643
  success: false,
1648
- error: errorMessage,
1644
+ error: `swarm_complete failed: ${errorMessage}`,
1649
1645
  failed_step: failedStep,
1650
1646
  bead_id: args.bead_id,
1651
1647
  agent_name: args.agent_name,
1652
1648
  coordinator_notified: notificationSent,
1653
1649
  stack_trace: errorStack?.slice(0, 500),
1650
+ hint: "Check the error message above. Common issues: bead not found, session not initialized.",
1654
1651
  context: {
1655
1652
  summary: args.summary,
1656
1653
  files_touched: args.files_touched || [],
@@ -1085,7 +1085,7 @@ describe("Tool Availability", () => {
1085
1085
 
1086
1086
  it("checks all tools at once", async () => {
1087
1087
  const availability = await checkAllTools();
1088
- expect(availability.size).toBe(6); // semantic-memory, cass, ubs, beads, swarm-mail, agent-mail
1088
+ expect(availability.size).toBe(7); // semantic-memory, cass, ubs, hive, beads, swarm-mail, agent-mail
1089
1089
  expect(availability.has("semantic-memory")).toBe(true);
1090
1090
  expect(availability.has("cass")).toBe(true);
1091
1091
  expect(availability.has("ubs")).toBe(true);
@@ -1420,7 +1420,7 @@ describe("Swarm Prompt V2 (with Swarm Mail/Beads)", () => {
1420
1420
  it("enforces swarm_complete over manual hive_close", () => {
1421
1421
  // Step 9: Use swarm_complete, not hive_close
1422
1422
  expect(SUBTASK_PROMPT_V2).toContain("swarm_complete");
1423
- expect(SUBTASK_PROMPT_V2).toContain("DO NOT manually close the bead");
1423
+ expect(SUBTASK_PROMPT_V2).toContain("DO NOT manually close the cell");
1424
1424
  expect(SUBTASK_PROMPT_V2).toContain("Use swarm_complete");
1425
1425
  });
1426
1426
  });
@@ -1583,6 +1583,92 @@ describe("Swarm Prompt V2 (with Swarm Mail/Beads)", () => {
1583
1583
  },
1584
1584
  );
1585
1585
 
1586
+ it.skipIf(!beadsAvailable)(
1587
+ "returns specific error message when bead_id not found",
1588
+ async () => {
1589
+ // Try to complete with a non-existent bead ID
1590
+ const result = await swarm_complete.execute(
1591
+ {
1592
+ project_key: "/tmp/test-bead-not-found",
1593
+ agent_name: "test-agent",
1594
+ bead_id: "bd-totally-fake-xyz123",
1595
+ summary: "This should fail with specific error",
1596
+ skip_verification: true,
1597
+ },
1598
+ mockContext,
1599
+ );
1600
+
1601
+ const parsed = JSON.parse(result);
1602
+
1603
+ // Should return structured error with specific message
1604
+ expect(parsed.success).toBe(false);
1605
+ expect(parsed.error).toBeDefined();
1606
+ // RED: This will fail - we currently get generic "Tool execution failed"
1607
+ // We want the error message to specifically mention the bead was not found
1608
+ expect(
1609
+ parsed.error.toLowerCase().includes("bead not found") ||
1610
+ parsed.error.toLowerCase().includes("not found"),
1611
+ ).toBe(true);
1612
+ expect(parsed.bead_id).toBe("bd-totally-fake-xyz123");
1613
+ },
1614
+ );
1615
+
1616
+ it.skipIf(!beadsAvailable)(
1617
+ "returns specific error when project_key is invalid/mismatched",
1618
+ async () => {
1619
+ // Create a real bead first
1620
+ const createResult =
1621
+ await Bun.$`bd create "Test project mismatch" -t task --json`
1622
+ .quiet()
1623
+ .nothrow();
1624
+
1625
+ if (createResult.exitCode !== 0) {
1626
+ console.warn(
1627
+ "Could not create bead:",
1628
+ createResult.stderr.toString(),
1629
+ );
1630
+ return;
1631
+ }
1632
+
1633
+ const bead = JSON.parse(createResult.stdout.toString());
1634
+
1635
+ try {
1636
+ // Try to complete with mismatched project_key
1637
+ const result = await swarm_complete.execute(
1638
+ {
1639
+ project_key: "/totally/wrong/project/path",
1640
+ agent_name: "test-agent",
1641
+ bead_id: bead.id,
1642
+ summary: "This should fail with project mismatch",
1643
+ skip_verification: true,
1644
+ },
1645
+ mockContext,
1646
+ );
1647
+
1648
+ const parsed = JSON.parse(result);
1649
+
1650
+ // Should return structured error with specific message about project mismatch
1651
+ expect(parsed.success).toBe(false);
1652
+ expect(parsed.error).toBeDefined();
1653
+ // RED: This will fail - we want specific validation error
1654
+ // Error should mention project mismatch or validation failure
1655
+ const errorLower = parsed.error.toLowerCase();
1656
+ expect(
1657
+ (errorLower.includes("project") &&
1658
+ (errorLower.includes("mismatch") ||
1659
+ errorLower.includes("invalid") ||
1660
+ errorLower.includes("not found"))) ||
1661
+ errorLower.includes("validation"),
1662
+ ).toBe(true);
1663
+ } finally {
1664
+ // Clean up
1665
+ await Bun.$`bd close ${bead.id} --reason "Test cleanup"`
1666
+ .quiet()
1667
+ .nothrow();
1668
+ }
1669
+ },
1670
+ );
1671
+
1586
1672
  it.skipIf(!beadsAvailable)(
1587
1673
  "includes message_sent status in response",
1588
1674
  async () => {
@@ -214,31 +214,13 @@ const toolCheckers: Record<ToolName, () => Promise<ToolStatus>> = {
214
214
  },
215
215
 
216
216
  // DEPRECATED: Use hive instead
217
- // Kept for backward compatibility only
217
+ // bd CLI is deprecated - always return false, use HiveAdapter instead
218
218
  beads: async () => {
219
- const exists = await commandExists("bd");
220
- if (!exists) {
221
- return {
222
- available: false,
223
- checkedAt: new Date().toISOString(),
224
- error: "bd command not found",
225
- };
226
- }
227
-
228
- try {
229
- // Just check if bd can run - don't require a repo
230
- const result = await Bun.$`bd --version`.quiet().nothrow();
231
- return {
232
- available: result.exitCode === 0,
233
- checkedAt: new Date().toISOString(),
234
- };
235
- } catch (e) {
236
- return {
237
- available: false,
238
- checkedAt: new Date().toISOString(),
239
- error: String(e),
240
- };
241
- }
219
+ return {
220
+ available: false,
221
+ checkedAt: new Date().toISOString(),
222
+ error: "bd CLI is deprecated - use hive_* tools with HiveAdapter instead",
223
+ };
242
224
  },
243
225
 
244
226
  "swarm-mail": async () => {