opencode-swarm-plugin 0.12.15 → 0.12.17

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.
package/src/beads.ts CHANGED
@@ -9,9 +9,89 @@
9
9
  * - Validate all output with Zod schemas
10
10
  * - Throw typed errors on failure
11
11
  * - Support atomic epic creation with rollback hints
12
+ *
13
+ * IMPORTANT: Call setBeadsWorkingDirectory() before using tools to ensure
14
+ * bd commands run in the correct project directory.
12
15
  */
13
16
  import { tool } from "@opencode-ai/plugin";
14
17
  import { z } from "zod";
18
+
19
+ // ============================================================================
20
+ // Working Directory Configuration
21
+ // ============================================================================
22
+
23
+ /**
24
+ * Module-level working directory for bd commands.
25
+ * Set this via setBeadsWorkingDirectory() before using tools.
26
+ * If not set, commands run in process.cwd() which may be wrong for plugins.
27
+ */
28
+ let beadsWorkingDirectory: string | null = null;
29
+
30
+ /**
31
+ * Set the working directory for all beads commands.
32
+ * Call this from the plugin initialization with the project directory.
33
+ *
34
+ * @param directory - Absolute path to the project directory
35
+ */
36
+ export function setBeadsWorkingDirectory(directory: string): void {
37
+ beadsWorkingDirectory = directory;
38
+ }
39
+
40
+ /**
41
+ * Get the current working directory for beads commands.
42
+ * Returns the configured directory or process.cwd() as fallback.
43
+ */
44
+ export function getBeadsWorkingDirectory(): string {
45
+ return beadsWorkingDirectory || process.cwd();
46
+ }
47
+
48
+ /**
49
+ * Run a bd command in the correct working directory.
50
+ * Uses Bun.spawn with cwd option to ensure commands run in project directory.
51
+ */
52
+ async function runBdCommand(
53
+ args: string[],
54
+ ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
55
+ const cwd = getBeadsWorkingDirectory();
56
+ const proc = Bun.spawn(["bd", ...args], {
57
+ cwd,
58
+ stdout: "pipe",
59
+ stderr: "pipe",
60
+ });
61
+
62
+ const [stdout, stderr] = await Promise.all([
63
+ new Response(proc.stdout).text(),
64
+ new Response(proc.stderr).text(),
65
+ ]);
66
+
67
+ const exitCode = await proc.exited;
68
+
69
+ return { exitCode, stdout, stderr };
70
+ }
71
+
72
+ /**
73
+ * Run a git command in the correct working directory.
74
+ */
75
+ async function runGitCommand(
76
+ args: string[],
77
+ ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
78
+ const cwd = getBeadsWorkingDirectory();
79
+ const proc = Bun.spawn(["git", ...args], {
80
+ cwd,
81
+ stdout: "pipe",
82
+ stderr: "pipe",
83
+ });
84
+
85
+ const [stdout, stderr] = await Promise.all([
86
+ new Response(proc.stdout).text(),
87
+ new Response(proc.stderr).text(),
88
+ ]);
89
+
90
+ const exitCode = await proc.exited;
91
+
92
+ return { exitCode, stdout, stderr };
93
+ }
94
+
15
95
  import {
16
96
  BeadSchema,
17
97
  BeadCreateArgsSchema,
@@ -159,19 +239,40 @@ export const beads_create = tool({
159
239
  const validated = BeadCreateArgsSchema.parse(args);
160
240
  const cmdParts = buildCreateCommand(validated);
161
241
 
162
- // Execute command
163
- const result = await Bun.$`${cmdParts}`.quiet().nothrow();
242
+ // Execute command in the correct working directory
243
+ const result = await runBdCommand(cmdParts.slice(1)); // Remove 'bd' prefix
164
244
 
165
245
  if (result.exitCode !== 0) {
166
246
  throw new BeadError(
167
- `Failed to create bead: ${result.stderr.toString()}`,
247
+ `Failed to create bead: ${result.stderr}`,
168
248
  cmdParts.join(" "),
169
249
  result.exitCode,
170
- result.stderr.toString(),
250
+ result.stderr,
171
251
  );
172
252
  }
173
253
 
174
- const bead = parseBead(result.stdout.toString());
254
+ // Validate output before parsing
255
+ const stdout = result.stdout.trim();
256
+ if (!stdout) {
257
+ throw new BeadError(
258
+ "bd create returned empty output",
259
+ cmdParts.join(" "),
260
+ 0,
261
+ "Empty stdout",
262
+ );
263
+ }
264
+
265
+ // Check for error messages in stdout (bd sometimes outputs errors to stdout)
266
+ if (stdout.startsWith("error:") || stdout.startsWith("Error:")) {
267
+ throw new BeadError(
268
+ `bd create failed: ${stdout}`,
269
+ cmdParts.join(" "),
270
+ 0,
271
+ stdout,
272
+ );
273
+ }
274
+
275
+ const bead = parseBead(stdout);
175
276
  return JSON.stringify(bead, null, 2);
176
277
  },
177
278
  });
@@ -210,17 +311,17 @@ export const beads_create_epic = tool({
210
311
  description: validated.epic_description,
211
312
  });
212
313
 
213
- const epicResult = await Bun.$`${epicCmd}`.quiet().nothrow();
314
+ const epicResult = await runBdCommand(epicCmd.slice(1)); // Remove 'bd' prefix
214
315
 
215
316
  if (epicResult.exitCode !== 0) {
216
317
  throw new BeadError(
217
- `Failed to create epic: ${epicResult.stderr.toString()}`,
318
+ `Failed to create epic: ${epicResult.stderr}`,
218
319
  epicCmd.join(" "),
219
320
  epicResult.exitCode,
220
321
  );
221
322
  }
222
323
 
223
- const epic = parseBead(epicResult.stdout.toString());
324
+ const epic = parseBead(epicResult.stdout);
224
325
  created.push(epic);
225
326
 
226
327
  // 2. Create subtasks
@@ -232,17 +333,17 @@ export const beads_create_epic = tool({
232
333
  parent_id: epic.id,
233
334
  });
234
335
 
235
- const subtaskResult = await Bun.$`${subtaskCmd}`.quiet().nothrow();
336
+ const subtaskResult = await runBdCommand(subtaskCmd.slice(1)); // Remove 'bd' prefix
236
337
 
237
338
  if (subtaskResult.exitCode !== 0) {
238
339
  throw new BeadError(
239
- `Failed to create subtask: ${subtaskResult.stderr.toString()}`,
340
+ `Failed to create subtask: ${subtaskResult.stderr}`,
240
341
  subtaskCmd.join(" "),
241
342
  subtaskResult.exitCode,
242
343
  );
243
344
  }
244
345
 
245
- const subtaskBead = parseBead(subtaskResult.stdout.toString());
346
+ const subtaskBead = parseBead(subtaskResult.stdout);
246
347
  created.push(subtaskBead);
247
348
  }
248
349
 
@@ -256,33 +357,53 @@ export const beads_create_epic = tool({
256
357
  } catch (error) {
257
358
  // Partial failure - execute rollback automatically
258
359
  const rollbackCommands: string[] = [];
360
+ const rollbackErrors: string[] = [];
259
361
 
260
362
  for (const bead of created) {
261
363
  try {
262
- const closeCmd = [
263
- "bd",
364
+ const closeArgs = [
264
365
  "close",
265
366
  bead.id,
266
367
  "--reason",
267
368
  "Rollback partial epic",
268
369
  "--json",
269
370
  ];
270
- await Bun.$`${closeCmd}`.quiet().nothrow();
271
- rollbackCommands.push(
272
- `bd close ${bead.id} --reason "Rollback partial epic"`,
273
- );
371
+ const rollbackResult = await runBdCommand(closeArgs);
372
+ if (rollbackResult.exitCode === 0) {
373
+ rollbackCommands.push(
374
+ `bd close ${bead.id} --reason "Rollback partial epic"`,
375
+ );
376
+ } else {
377
+ rollbackErrors.push(
378
+ `${bead.id}: exit ${rollbackResult.exitCode} - ${rollbackResult.stderr.trim()}`,
379
+ );
380
+ }
274
381
  } catch (rollbackError) {
275
- // Log rollback failure but continue
382
+ // Log rollback failure and collect error
383
+ const errMsg =
384
+ rollbackError instanceof Error
385
+ ? rollbackError.message
386
+ : String(rollbackError);
276
387
  console.error(`Failed to rollback bead ${bead.id}:`, rollbackError);
388
+ rollbackErrors.push(`${bead.id}: ${errMsg}`);
277
389
  }
278
390
  }
279
391
 
280
- // Throw error with rollback info
392
+ // Throw error with rollback info including any failures
281
393
  const errorMsg = error instanceof Error ? error.message : String(error);
282
- const rollbackInfo =
283
- rollbackCommands.length > 0
284
- ? `\n\nRolled back ${rollbackCommands.length} bead(s):\n${rollbackCommands.join("\n")}`
285
- : "\n\nNo beads to rollback.";
394
+ let rollbackInfo = "";
395
+
396
+ if (rollbackCommands.length > 0) {
397
+ rollbackInfo += `\n\nRolled back ${rollbackCommands.length} bead(s):\n${rollbackCommands.join("\n")}`;
398
+ }
399
+
400
+ if (rollbackErrors.length > 0) {
401
+ rollbackInfo += `\n\nRollback failures (${rollbackErrors.length}):\n${rollbackErrors.join("\n")}`;
402
+ }
403
+
404
+ if (!rollbackInfo) {
405
+ rollbackInfo = "\n\nNo beads to rollback.";
406
+ }
286
407
 
287
408
  throw new BeadError(
288
409
  `Epic creation failed: ${errorMsg}${rollbackInfo}`,
@@ -333,17 +454,17 @@ export const beads_query = tool({
333
454
  }
334
455
  }
335
456
 
336
- const result = await Bun.$`${cmd}`.quiet().nothrow();
457
+ const result = await runBdCommand(cmd.slice(1)); // Remove 'bd' prefix
337
458
 
338
459
  if (result.exitCode !== 0) {
339
460
  throw new BeadError(
340
- `Failed to query beads: ${result.stderr.toString()}`,
461
+ `Failed to query beads: ${result.stderr}`,
341
462
  cmd.join(" "),
342
463
  result.exitCode,
343
464
  );
344
465
  }
345
466
 
346
- const beads = parseBeads(result.stdout.toString());
467
+ const beads = parseBeads(result.stdout);
347
468
  const limited = beads.slice(0, validated.limit);
348
469
 
349
470
  return JSON.stringify(limited, null, 2);
@@ -385,17 +506,17 @@ export const beads_update = tool({
385
506
  }
386
507
  cmd.push("--json");
387
508
 
388
- const result = await Bun.$`${cmd}`.quiet().nothrow();
509
+ const result = await runBdCommand(cmd.slice(1)); // Remove 'bd' prefix
389
510
 
390
511
  if (result.exitCode !== 0) {
391
512
  throw new BeadError(
392
- `Failed to update bead: ${result.stderr.toString()}`,
513
+ `Failed to update bead: ${result.stderr}`,
393
514
  cmd.join(" "),
394
515
  result.exitCode,
395
516
  );
396
517
  }
397
518
 
398
- const bead = parseBead(result.stdout.toString());
519
+ const bead = parseBead(result.stdout);
399
520
  return JSON.stringify(bead, null, 2);
400
521
  },
401
522
  });
@@ -421,17 +542,17 @@ export const beads_close = tool({
421
542
  "--json",
422
543
  ];
423
544
 
424
- const result = await Bun.$`${cmd}`.quiet().nothrow();
545
+ const result = await runBdCommand(cmd.slice(1)); // Remove 'bd' prefix
425
546
 
426
547
  if (result.exitCode !== 0) {
427
548
  throw new BeadError(
428
- `Failed to close bead: ${result.stderr.toString()}`,
549
+ `Failed to close bead: ${result.stderr}`,
429
550
  cmd.join(" "),
430
551
  result.exitCode,
431
552
  );
432
553
  }
433
554
 
434
- const bead = parseBead(result.stdout.toString());
555
+ const bead = parseBead(result.stdout);
435
556
  return `Closed ${bead.id}: ${validated.reason}`;
436
557
  },
437
558
  });
@@ -446,19 +567,23 @@ export const beads_start = tool({
446
567
  id: tool.schema.string().describe("Bead ID"),
447
568
  },
448
569
  async execute(args, ctx) {
449
- const cmd = ["bd", "update", args.id, "--status", "in_progress", "--json"];
450
-
451
- const result = await Bun.$`${cmd}`.quiet().nothrow();
570
+ const result = await runBdCommand([
571
+ "update",
572
+ args.id,
573
+ "--status",
574
+ "in_progress",
575
+ "--json",
576
+ ]);
452
577
 
453
578
  if (result.exitCode !== 0) {
454
579
  throw new BeadError(
455
- `Failed to start bead: ${result.stderr.toString()}`,
456
- cmd.join(" "),
580
+ `Failed to start bead: ${result.stderr}`,
581
+ `bd update ${args.id} --status in_progress --json`,
457
582
  result.exitCode,
458
583
  );
459
584
  }
460
585
 
461
- const bead = parseBead(result.stdout.toString());
586
+ const bead = parseBead(result.stdout);
462
587
  return `Started: ${bead.id}`;
463
588
  },
464
589
  });
@@ -470,19 +595,17 @@ export const beads_ready = tool({
470
595
  description: "Get the next ready bead (unblocked, highest priority)",
471
596
  args: {},
472
597
  async execute(args, ctx) {
473
- const cmd = ["bd", "ready", "--json"];
474
-
475
- const result = await Bun.$`${cmd}`.quiet().nothrow();
598
+ const result = await runBdCommand(["ready", "--json"]);
476
599
 
477
600
  if (result.exitCode !== 0) {
478
601
  throw new BeadError(
479
- `Failed to get ready beads: ${result.stderr.toString()}`,
480
- cmd.join(" "),
602
+ `Failed to get ready beads: ${result.stderr}`,
603
+ "bd ready --json",
481
604
  result.exitCode,
482
605
  );
483
606
  }
484
607
 
485
- const beads = parseBeads(result.stdout.toString());
608
+ const beads = parseBeads(result.stdout);
486
609
 
487
610
  if (beads.length === 0) {
488
611
  return "No ready beads";
@@ -510,14 +633,17 @@ export const beads_sync = tool({
510
633
 
511
634
  /**
512
635
  * Helper to run a command with timeout
636
+ * Properly clears the timeout to avoid lingering timers
513
637
  */
514
638
  const withTimeout = async <T>(
515
639
  promise: Promise<T>,
516
640
  timeoutMs: number,
517
641
  operation: string,
518
642
  ): Promise<T> => {
519
- const timeoutPromise = new Promise<never>((_, reject) =>
520
- setTimeout(
643
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
644
+
645
+ const timeoutPromise = new Promise<never>((_, reject) => {
646
+ timeoutId = setTimeout(
521
647
  () =>
522
648
  reject(
523
649
  new BeadError(
@@ -526,21 +652,28 @@ export const beads_sync = tool({
526
652
  ),
527
653
  ),
528
654
  timeoutMs,
529
- ),
530
- );
531
- return Promise.race([promise, timeoutPromise]);
655
+ );
656
+ });
657
+
658
+ try {
659
+ return await Promise.race([promise, timeoutPromise]);
660
+ } finally {
661
+ if (timeoutId !== undefined) {
662
+ clearTimeout(timeoutId);
663
+ }
664
+ }
532
665
  };
533
666
 
534
667
  // 1. Pull if requested
535
668
  if (autoPull) {
536
669
  const pullResult = await withTimeout(
537
- Bun.$`git pull --rebase`.quiet().nothrow(),
670
+ runGitCommand(["pull", "--rebase"]),
538
671
  TIMEOUT_MS,
539
672
  "git pull --rebase",
540
673
  );
541
674
  if (pullResult.exitCode !== 0) {
542
675
  throw new BeadError(
543
- `Failed to pull: ${pullResult.stderr.toString()}`,
676
+ `Failed to pull: ${pullResult.stderr}`,
544
677
  "git pull --rebase",
545
678
  pullResult.exitCode,
546
679
  );
@@ -549,13 +682,13 @@ export const beads_sync = tool({
549
682
 
550
683
  // 2. Sync beads
551
684
  const syncResult = await withTimeout(
552
- Bun.$`bd sync`.quiet().nothrow(),
685
+ runBdCommand(["sync"]),
553
686
  TIMEOUT_MS,
554
687
  "bd sync",
555
688
  );
556
689
  if (syncResult.exitCode !== 0) {
557
690
  throw new BeadError(
558
- `Failed to sync beads: ${syncResult.stderr.toString()}`,
691
+ `Failed to sync beads: ${syncResult.stderr}`,
559
692
  "bd sync",
560
693
  syncResult.exitCode,
561
694
  );
@@ -563,21 +696,21 @@ export const beads_sync = tool({
563
696
 
564
697
  // 3. Push
565
698
  const pushResult = await withTimeout(
566
- Bun.$`git push`.quiet().nothrow(),
699
+ runGitCommand(["push"]),
567
700
  TIMEOUT_MS,
568
701
  "git push",
569
702
  );
570
703
  if (pushResult.exitCode !== 0) {
571
704
  throw new BeadError(
572
- `Failed to push: ${pushResult.stderr.toString()}`,
705
+ `Failed to push: ${pushResult.stderr}`,
573
706
  "git push",
574
707
  pushResult.exitCode,
575
708
  );
576
709
  }
577
710
 
578
711
  // 4. Verify clean state
579
- const statusResult = await Bun.$`git status --porcelain`.quiet().nothrow();
580
- const status = statusResult.stdout.toString().trim();
712
+ const statusResult = await runGitCommand(["status", "--porcelain"]);
713
+ const status = statusResult.stdout.trim();
581
714
 
582
715
  if (status !== "") {
583
716
  return `Beads synced and pushed, but working directory not clean:\n${status}`;
@@ -599,19 +732,17 @@ export const beads_link_thread = tool({
599
732
  async execute(args, ctx) {
600
733
  // Update bead description to include thread link
601
734
  // This is a workaround since bd doesn't have native metadata support
602
- const queryResult = await Bun.$`bd show ${args.bead_id} --json`
603
- .quiet()
604
- .nothrow();
735
+ const queryResult = await runBdCommand(["show", args.bead_id, "--json"]);
605
736
 
606
737
  if (queryResult.exitCode !== 0) {
607
738
  throw new BeadError(
608
- `Failed to get bead: ${queryResult.stderr.toString()}`,
739
+ `Failed to get bead: ${queryResult.stderr}`,
609
740
  `bd show ${args.bead_id} --json`,
610
741
  queryResult.exitCode,
611
742
  );
612
743
  }
613
744
 
614
- const bead = parseBead(queryResult.stdout.toString());
745
+ const bead = parseBead(queryResult.stdout);
615
746
  const existingDesc = bead.description || "";
616
747
 
617
748
  // Add thread link if not already present
@@ -624,14 +755,17 @@ export const beads_link_thread = tool({
624
755
  ? `${existingDesc}\n\n${threadMarker}`
625
756
  : threadMarker;
626
757
 
627
- const updateResult =
628
- await Bun.$`bd update ${args.bead_id} -d ${newDesc} --json`
629
- .quiet()
630
- .nothrow();
758
+ const updateResult = await runBdCommand([
759
+ "update",
760
+ args.bead_id,
761
+ "-d",
762
+ newDesc,
763
+ "--json",
764
+ ]);
631
765
 
632
766
  if (updateResult.exitCode !== 0) {
633
767
  throw new BeadError(
634
- `Failed to update bead: ${updateResult.stderr.toString()}`,
768
+ `Failed to update bead: ${updateResult.stderr}`,
635
769
  `bd update ${args.bead_id} -d ...`,
636
770
  updateResult.exitCode,
637
771
  );
package/src/index.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  */
23
23
  import type { Plugin, PluginInput, Hooks } from "@opencode-ai/plugin";
24
24
 
25
- import { beadsTools } from "./beads";
25
+ import { beadsTools, setBeadsWorkingDirectory } from "./beads";
26
26
  import {
27
27
  agentMailTools,
28
28
  type AgentMailState,
@@ -48,7 +48,11 @@ import { repoCrawlTools } from "./repo-crawl";
48
48
  export const SwarmPlugin: Plugin = async (
49
49
  input: PluginInput,
50
50
  ): Promise<Hooks> => {
51
- const { $ } = input;
51
+ const { $, directory } = input;
52
+
53
+ // Set the working directory for beads commands
54
+ // This ensures bd runs in the project directory, not ~/.config/opencode
55
+ setBeadsWorkingDirectory(directory);
52
56
 
53
57
  /** Track active sessions for cleanup */
54
58
  let activeAgentMailState: AgentMailState | null = null;
package/src/storage.ts CHANGED
@@ -248,6 +248,9 @@ export class SemanticMemoryStorage implements LearningStorage {
248
248
  const result = await execSemanticMemory(args);
249
249
 
250
250
  if (result.exitCode !== 0) {
251
+ console.warn(
252
+ `[storage] semantic-memory find() failed with exit code ${result.exitCode}: ${result.stderr.toString().trim()}`,
253
+ );
251
254
  return [];
252
255
  }
253
256
 
@@ -268,7 +271,10 @@ export class SemanticMemoryStorage implements LearningStorage {
268
271
  return content;
269
272
  }
270
273
  });
271
- } catch {
274
+ } catch (error) {
275
+ console.warn(
276
+ `[storage] Failed to parse semantic-memory find() output: ${error instanceof Error ? error.message : String(error)}`,
277
+ );
272
278
  return [];
273
279
  }
274
280
  }
@@ -282,6 +288,9 @@ export class SemanticMemoryStorage implements LearningStorage {
282
288
  ]);
283
289
 
284
290
  if (result.exitCode !== 0) {
291
+ console.warn(
292
+ `[storage] semantic-memory list() failed with exit code ${result.exitCode}: ${result.stderr.toString().trim()}`,
293
+ );
285
294
  return [];
286
295
  }
287
296
 
@@ -300,7 +309,10 @@ export class SemanticMemoryStorage implements LearningStorage {
300
309
  return content;
301
310
  }
302
311
  });
303
- } catch {
312
+ } catch (error) {
313
+ console.warn(
314
+ `[storage] Failed to parse semantic-memory list() output: ${error instanceof Error ? error.message : String(error)}`,
315
+ );
304
316
  return [];
305
317
  }
306
318
  }