opencode-swarm-plugin 0.25.2 → 0.25.3

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
@@ -1,27 +1,34 @@
1
1
  /**
2
- * Beads Module - Type-safe wrappers around the `bd` CLI
2
+ * Beads Module - Type-safe wrappers using BeadsAdapter
3
3
  *
4
4
  * This module provides validated, type-safe operations for the beads
5
- * issue tracker. All responses are parsed and validated with Zod schemas.
5
+ * issue tracker using the BeadsAdapter from swarm-mail.
6
6
  *
7
7
  * Key principles:
8
- * - Always use `--json` flag for bd commands
9
- * - Validate all output with Zod schemas
8
+ * - Use BeadsAdapter for all operations (no CLI commands)
9
+ * - Validate all inputs with Zod schemas
10
10
  * - Throw typed errors on failure
11
- * - Support atomic epic creation with rollback hints
11
+ * - Support atomic epic creation with rollback
12
12
  *
13
13
  * IMPORTANT: Call setBeadsWorkingDirectory() before using tools to ensure
14
- * bd commands run in the correct project directory.
14
+ * operations run in the correct project directory.
15
15
  */
16
16
  import { tool } from "@opencode-ai/plugin";
17
17
  import { z } from "zod";
18
+ import {
19
+ createBeadsAdapter,
20
+ FlushManager,
21
+ type BeadsAdapter,
22
+ type Bead as AdapterBead,
23
+ getSwarmMail,
24
+ } from "swarm-mail";
18
25
 
19
26
  // ============================================================================
20
27
  // Working Directory Configuration
21
28
  // ============================================================================
22
29
 
23
30
  /**
24
- * Module-level working directory for bd commands.
31
+ * Module-level working directory for beads commands.
25
32
  * Set this via setBeadsWorkingDirectory() before using tools.
26
33
  * If not set, commands run in process.cwd() which may be wrong for plugins.
27
34
  */
@@ -45,30 +52,6 @@ export function getBeadsWorkingDirectory(): string {
45
52
  return beadsWorkingDirectory || process.cwd();
46
53
  }
47
54
 
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
55
  /**
73
56
  * Run a git command in the correct working directory.
74
57
  */
@@ -134,93 +117,58 @@ export class BeadValidationError extends Error {
134
117
  }
135
118
  }
136
119
 
120
+ // ============================================================================
121
+ // Adapter Singleton
122
+ // ============================================================================
123
+
137
124
  /**
138
- * Build a bd create command from args
139
- *
140
- * Note: Bun's `$` template literal properly escapes arguments when passed as array.
141
- * Each array element is treated as a separate argument, preventing shell injection.
142
- * Example: ["bd", "create", "; rm -rf /"] becomes: bd create "; rm -rf /"
125
+ * Lazy singleton for BeadsAdapter instances
126
+ * Maps projectKey -> BeadsAdapter
143
127
  */
144
- function buildCreateCommand(args: BeadCreateArgs): string[] {
145
- const parts = ["bd", "create", args.title];
146
-
147
- if (args.type && args.type !== "task") {
148
- parts.push("-t", args.type);
149
- }
150
-
151
- if (args.priority !== undefined && args.priority !== 2) {
152
- parts.push("-p", args.priority.toString());
153
- }
128
+ const adapterCache = new Map<string, BeadsAdapter>();
154
129
 
155
- if (args.description) {
156
- parts.push("-d", args.description);
130
+ /**
131
+ * Get or create a BeadsAdapter instance for a project
132
+ * Exported for testing - allows tests to verify state directly
133
+ */
134
+ export async function getBeadsAdapter(projectKey: string): Promise<BeadsAdapter> {
135
+ if (adapterCache.has(projectKey)) {
136
+ return adapterCache.get(projectKey)!;
157
137
  }
158
138
 
159
- if (args.parent_id) {
160
- parts.push("--parent", args.parent_id);
161
- }
139
+ const swarmMail = await getSwarmMail(projectKey);
140
+ const db = await swarmMail.getDatabase();
141
+ const adapter = createBeadsAdapter(db, projectKey);
162
142
 
163
- // Custom ID for human-readable bead names (e.g., 'phase-0', 'phase-1.e2e-test')
164
- if (args.id) {
165
- parts.push("--id", args.id);
166
- }
143
+ // Run migrations to ensure schema exists
144
+ await adapter.runMigrations();
167
145
 
168
- parts.push("--json");
169
- return parts;
146
+ adapterCache.set(projectKey, adapter);
147
+ return adapter;
170
148
  }
171
149
 
172
150
  /**
173
- * Parse and validate bead JSON output
174
- * Handles both object and array responses (CLI may return either)
151
+ * Format adapter bead for output (map field names)
152
+ * Adapter uses: type, created_at/updated_at (timestamps)
153
+ * Schema expects: issue_type, created_at/updated_at (ISO strings)
175
154
  */
176
- function parseBead(output: string): Bead {
177
- try {
178
- const parsed = JSON.parse(output);
179
- // CLI commands like `bd close`, `bd update` return arrays even for single items
180
- const data = Array.isArray(parsed) ? parsed[0] : parsed;
181
- if (!data) {
182
- throw new BeadError(
183
- "No bead data in response. The bd CLI may not be installed or returned unexpected output. Try: Run 'bd --version' to verify installation, or check if .beads/ directory exists in project.",
184
- "parse",
185
- );
186
- }
187
- return BeadSchema.parse(data);
188
- } catch (error) {
189
- if (error instanceof z.ZodError) {
190
- throw new BeadValidationError(
191
- `Invalid bead data: ${error.message}`,
192
- error,
193
- );
194
- }
195
- if (error instanceof BeadError) {
196
- throw error;
197
- }
198
- throw new BeadError(
199
- `Failed to parse bead JSON because output is malformed. Try: Check if bd CLI is up to date with 'bd --version' (need v1.0.0+), or inspect output: ${output.slice(0, 100)}`,
200
- "parse",
201
- );
202
- }
203
- }
204
-
205
- /**
206
- * Parse and validate array of beads
207
- */
208
- function parseBeads(output: string): Bead[] {
209
- try {
210
- const parsed = JSON.parse(output);
211
- return z.array(BeadSchema).parse(parsed);
212
- } catch (error) {
213
- if (error instanceof z.ZodError) {
214
- throw new BeadValidationError(
215
- `Invalid beads data: ${error.message}`,
216
- error,
217
- );
218
- }
219
- throw new BeadError(
220
- `Failed to parse beads JSON because output is malformed. Try: Check if bd CLI is up to date with 'bd --version' (need v1.0.0+), or inspect output: ${output.slice(0, 100)}`,
221
- "parse",
222
- );
223
- }
155
+ function formatBeadForOutput(adapterBead: AdapterBead): Record<string, unknown> {
156
+ return {
157
+ id: adapterBead.id,
158
+ title: adapterBead.title,
159
+ description: adapterBead.description || "",
160
+ status: adapterBead.status,
161
+ priority: adapterBead.priority,
162
+ issue_type: adapterBead.type, // Adapter: type Schema: issue_type
163
+ created_at: new Date(adapterBead.created_at).toISOString(),
164
+ updated_at: new Date(adapterBead.updated_at).toISOString(),
165
+ closed_at: adapterBead.closed_at
166
+ ? new Date(adapterBead.closed_at).toISOString()
167
+ : undefined,
168
+ parent_id: adapterBead.parent_id || undefined,
169
+ dependencies: [], // TODO: fetch from adapter if needed
170
+ metadata: {},
171
+ };
224
172
  }
225
173
 
226
174
  // ============================================================================
@@ -252,43 +200,30 @@ export const beads_create = tool({
252
200
  },
253
201
  async execute(args, ctx) {
254
202
  const validated = BeadCreateArgsSchema.parse(args);
255
- const cmdParts = buildCreateCommand(validated);
203
+ const projectKey = getBeadsWorkingDirectory();
204
+ const adapter = await getBeadsAdapter(projectKey);
256
205
 
257
- // Execute command in the correct working directory
258
- const result = await runBdCommand(cmdParts.slice(1)); // Remove 'bd' prefix
259
-
260
- if (result.exitCode !== 0) {
261
- throw new BeadError(
262
- `Failed to create bead because bd command exited with code ${result.exitCode}. Error: ${result.stderr}. Try: Check if beads initialized with 'bd init' in project root, or verify .beads/ directory exists.`,
263
- cmdParts.join(" "),
264
- result.exitCode,
265
- result.stderr,
266
- );
267
- }
206
+ try {
207
+ const bead = await adapter.createBead(projectKey, {
208
+ title: validated.title,
209
+ type: validated.type || "task",
210
+ priority: validated.priority ?? 2,
211
+ description: validated.description,
212
+ parent_id: validated.parent_id,
213
+ });
268
214
 
269
- // Validate output before parsing
270
- const stdout = result.stdout.trim();
271
- if (!stdout) {
272
- throw new BeadError(
273
- "bd create returned empty output because command produced no response. Try: Check if bd is properly installed with 'bd --version', or run 'bd list' to test basic functionality.",
274
- cmdParts.join(" "),
275
- 0,
276
- "Empty stdout",
277
- );
278
- }
215
+ // Mark dirty for export
216
+ await adapter.markDirty(projectKey, bead.id);
279
217
 
280
- // Check for error messages in stdout (bd sometimes outputs errors to stdout)
281
- if (stdout.startsWith("error:") || stdout.startsWith("Error:")) {
218
+ const formatted = formatBeadForOutput(bead);
219
+ return JSON.stringify(formatted, null, 2);
220
+ } catch (error) {
221
+ const message = error instanceof Error ? error.message : String(error);
282
222
  throw new BeadError(
283
- `bd create failed because command returned error in stdout: ${stdout}. Try: Check error message above, verify beads initialized with 'bd init', or check .beads/issues.jsonl for corruption.`,
284
- cmdParts.join(" "),
285
- 0,
286
- stdout,
223
+ `Failed to create bead: ${message}`,
224
+ "beads_create",
287
225
  );
288
226
  }
289
-
290
- const bead = parseBead(stdout);
291
- return JSON.stringify(bead, null, 2);
292
227
  },
293
228
  });
294
229
 
@@ -345,66 +280,37 @@ export const beads_create_epic = tool({
345
280
  },
346
281
  async execute(args, ctx) {
347
282
  const validated = EpicCreateArgsSchema.parse(args);
348
- const created: Bead[] = [];
283
+ const projectKey = getBeadsWorkingDirectory();
284
+ const adapter = await getBeadsAdapter(projectKey);
285
+ const created: AdapterBead[] = [];
349
286
 
350
287
  try {
351
288
  // 1. Create epic
352
- const epicCmd = buildCreateCommand({
289
+ const epic = await adapter.createBead(projectKey, {
353
290
  title: validated.epic_title,
354
291
  type: "epic",
355
292
  priority: 1,
356
293
  description: validated.epic_description,
357
- id: validated.epic_id,
358
294
  });
359
-
360
- const epicResult = await runBdCommand(epicCmd.slice(1)); // Remove 'bd' prefix
361
-
362
- if (epicResult.exitCode !== 0) {
363
- throw new BeadError(
364
- `Failed to create epic because bd command failed: ${epicResult.stderr}. Try: Verify beads initialized with 'bd init', check if .beads/ directory is writable, or run 'bd list' to test basic functionality.`,
365
- epicCmd.join(" "),
366
- epicResult.exitCode,
367
- );
368
- }
369
-
370
- const epic = parseBead(epicResult.stdout);
295
+ await adapter.markDirty(projectKey, epic.id);
371
296
  created.push(epic);
372
297
 
373
298
  // 2. Create subtasks
374
299
  for (const subtask of validated.subtasks) {
375
- // Build subtask ID: if epic has custom ID and subtask has suffix, combine them
376
- // e.g., epic_id='phase-0', id_suffix='e2e-test' → 'phase-0.e2e-test'
377
- let subtaskId: string | undefined;
378
- if (validated.epic_id && subtask.id_suffix) {
379
- subtaskId = `${validated.epic_id}.${subtask.id_suffix}`;
380
- }
381
-
382
- const subtaskCmd = buildCreateCommand({
300
+ const subtaskBead = await adapter.createBead(projectKey, {
383
301
  title: subtask.title,
384
302
  type: "task",
385
303
  priority: subtask.priority ?? 2,
386
304
  parent_id: epic.id,
387
- id: subtaskId,
388
305
  });
389
-
390
- const subtaskResult = await runBdCommand(subtaskCmd.slice(1)); // Remove 'bd' prefix
391
-
392
- if (subtaskResult.exitCode !== 0) {
393
- throw new BeadError(
394
- `Failed to create subtask because bd command failed: ${subtaskResult.stderr}. Try: Check if parent epic exists with 'bd show ${epic.id}', verify .beads/issues.jsonl is not corrupted, or check for invalid characters in title.`,
395
- subtaskCmd.join(" "),
396
- subtaskResult.exitCode,
397
- );
398
- }
399
-
400
- const subtaskBead = parseBead(subtaskResult.stdout);
306
+ await adapter.markDirty(projectKey, subtaskBead.id);
401
307
  created.push(subtaskBead);
402
308
  }
403
309
 
404
310
  const result: EpicCreateResult = {
405
311
  success: true,
406
- epic,
407
- subtasks: created.slice(1),
312
+ epic: formatBeadForOutput(epic) as Bead,
313
+ subtasks: created.slice(1).map((b) => formatBeadForOutput(b) as Bead),
408
314
  };
409
315
 
410
316
  // Emit DecompositionGeneratedEvent for learning system
@@ -436,31 +342,15 @@ export const beads_create_epic = tool({
436
342
 
437
343
  return JSON.stringify(result, null, 2);
438
344
  } catch (error) {
439
- // Partial failure - execute rollback automatically
440
- const rollbackCommands: string[] = [];
345
+ // Partial failure - rollback via deleteBead
441
346
  const rollbackErrors: string[] = [];
442
347
 
443
348
  for (const bead of created) {
444
349
  try {
445
- const closeArgs = [
446
- "close",
447
- bead.id,
448
- "--reason",
449
- "Rollback partial epic",
450
- "--json",
451
- ];
452
- const rollbackResult = await runBdCommand(closeArgs);
453
- if (rollbackResult.exitCode === 0) {
454
- rollbackCommands.push(
455
- `bd close ${bead.id} --reason "Rollback partial epic"`,
456
- );
457
- } else {
458
- rollbackErrors.push(
459
- `${bead.id}: exit ${rollbackResult.exitCode} - ${rollbackResult.stderr.trim()}`,
460
- );
461
- }
350
+ await adapter.deleteBead(projectKey, bead.id, {
351
+ reason: "Rollback partial epic",
352
+ });
462
353
  } catch (rollbackError) {
463
- // Log rollback failure and collect error
464
354
  const errMsg =
465
355
  rollbackError instanceof Error
466
356
  ? rollbackError.message
@@ -470,24 +360,15 @@ export const beads_create_epic = tool({
470
360
  }
471
361
  }
472
362
 
473
- // Throw error with rollback info including any failures
474
363
  const errorMsg = error instanceof Error ? error.message : String(error);
475
- let rollbackInfo = "";
476
-
477
- if (rollbackCommands.length > 0) {
478
- rollbackInfo += `\n\nRolled back ${rollbackCommands.length} bead(s):\n${rollbackCommands.join("\n")}`;
479
- }
364
+ let rollbackInfo = `\n\nRolled back ${created.length - rollbackErrors.length} bead(s)`;
480
365
 
481
366
  if (rollbackErrors.length > 0) {
482
367
  rollbackInfo += `\n\nRollback failures (${rollbackErrors.length}):\n${rollbackErrors.join("\n")}`;
483
368
  }
484
369
 
485
- if (!rollbackInfo) {
486
- rollbackInfo = "\n\nNo beads to rollback.";
487
- }
488
-
489
370
  throw new BeadError(
490
- `Epic creation failed: ${errorMsg}${rollbackInfo}. Try: If rollback failed, manually close beads with 'bd close <id> --reason "Rollback"', check .beads/issues.jsonl for partial state, or re-run beads_create_epic with corrected parameters.`,
371
+ `Epic creation failed: ${errorMsg}${rollbackInfo}`,
491
372
  "beads_create_epic",
492
373
  1,
493
374
  );
@@ -520,35 +401,32 @@ export const beads_query = tool({
520
401
  },
521
402
  async execute(args, ctx) {
522
403
  const validated = BeadQueryArgsSchema.parse(args);
404
+ const projectKey = getBeadsWorkingDirectory();
405
+ const adapter = await getBeadsAdapter(projectKey);
523
406
 
524
- let cmd: string[];
525
-
526
- if (validated.ready) {
527
- cmd = ["bd", "ready", "--json"];
528
- } else {
529
- cmd = ["bd", "list", "--json"];
530
- if (validated.status) {
531
- cmd.push("--status", validated.status);
532
- }
533
- if (validated.type) {
534
- cmd.push("--type", validated.type);
407
+ try {
408
+ let beads: AdapterBead[];
409
+
410
+ if (validated.ready) {
411
+ const readyBead = await adapter.getNextReadyBead(projectKey);
412
+ beads = readyBead ? [readyBead] : [];
413
+ } else {
414
+ beads = await adapter.queryBeads(projectKey, {
415
+ status: validated.status,
416
+ type: validated.type,
417
+ limit: validated.limit || 20,
418
+ });
535
419
  }
536
- }
537
-
538
- const result = await runBdCommand(cmd.slice(1)); // Remove 'bd' prefix
539
420
 
540
- if (result.exitCode !== 0) {
421
+ const formatted = beads.map((b) => formatBeadForOutput(b));
422
+ return JSON.stringify(formatted, null, 2);
423
+ } catch (error) {
424
+ const message = error instanceof Error ? error.message : String(error);
541
425
  throw new BeadError(
542
- `Failed to query beads because bd command failed: ${result.stderr}. Try: Check if beads initialized with 'bd init', verify .beads/ directory exists, or run 'bd --version' to check CLI version.`,
543
- cmd.join(" "),
544
- result.exitCode,
426
+ `Failed to query beads: ${message}`,
427
+ "beads_query",
545
428
  );
546
429
  }
547
-
548
- const beads = parseBeads(result.stdout);
549
- const limited = beads.slice(0, validated.limit);
550
-
551
- return JSON.stringify(limited, null, 2);
552
430
  },
553
431
  });
554
432
 
@@ -573,32 +451,50 @@ export const beads_update = tool({
573
451
  },
574
452
  async execute(args, ctx) {
575
453
  const validated = BeadUpdateArgsSchema.parse(args);
454
+ const projectKey = getBeadsWorkingDirectory();
455
+ const adapter = await getBeadsAdapter(projectKey);
576
456
 
577
- const cmd = ["bd", "update", validated.id];
457
+ try {
458
+ let bead: AdapterBead;
578
459
 
579
- if (validated.status) {
580
- cmd.push("--status", validated.status);
581
- }
582
- if (validated.description) {
583
- cmd.push("-d", validated.description);
584
- }
585
- if (validated.priority !== undefined) {
586
- cmd.push("-p", validated.priority.toString());
587
- }
588
- cmd.push("--json");
460
+ // Status changes use changeBeadStatus, other fields use updateBead
461
+ if (validated.status) {
462
+ bead = await adapter.changeBeadStatus(
463
+ projectKey,
464
+ validated.id,
465
+ validated.status,
466
+ );
467
+ }
468
+
469
+ // Update other fields if provided
470
+ if (validated.description !== undefined || validated.priority !== undefined) {
471
+ bead = await adapter.updateBead(projectKey, validated.id, {
472
+ description: validated.description,
473
+ priority: validated.priority,
474
+ });
475
+ } else if (!validated.status) {
476
+ // No changes requested
477
+ const existingBead = await adapter.getBead(projectKey, validated.id);
478
+ if (!existingBead) {
479
+ throw new BeadError(
480
+ `Bead not found: ${validated.id}`,
481
+ "beads_update",
482
+ );
483
+ }
484
+ bead = existingBead;
485
+ }
589
486
 
590
- const result = await runBdCommand(cmd.slice(1)); // Remove 'bd' prefix
487
+ await adapter.markDirty(projectKey, validated.id);
591
488
 
592
- if (result.exitCode !== 0) {
489
+ const formatted = formatBeadForOutput(bead!);
490
+ return JSON.stringify(formatted, null, 2);
491
+ } catch (error) {
492
+ const message = error instanceof Error ? error.message : String(error);
593
493
  throw new BeadError(
594
- `Failed to update bead because bd command failed: ${result.stderr}. Try: Verify bead exists with 'bd show ${validated.id}', check for invalid status values, or inspect .beads/issues.jsonl for corruption.`,
595
- cmd.join(" "),
596
- result.exitCode,
494
+ `Failed to update bead: ${message}`,
495
+ "beads_update",
597
496
  );
598
497
  }
599
-
600
- const bead = parseBead(result.stdout);
601
- return JSON.stringify(bead, null, 2);
602
498
  },
603
499
  });
604
500
 
@@ -613,28 +509,26 @@ export const beads_close = tool({
613
509
  },
614
510
  async execute(args, ctx) {
615
511
  const validated = BeadCloseArgsSchema.parse(args);
512
+ const projectKey = getBeadsWorkingDirectory();
513
+ const adapter = await getBeadsAdapter(projectKey);
616
514
 
617
- const cmd = [
618
- "bd",
619
- "close",
620
- validated.id,
621
- "--reason",
622
- validated.reason,
623
- "--json",
624
- ];
515
+ try {
516
+ const bead = await adapter.closeBead(
517
+ projectKey,
518
+ validated.id,
519
+ validated.reason,
520
+ );
625
521
 
626
- const result = await runBdCommand(cmd.slice(1)); // Remove 'bd' prefix
522
+ await adapter.markDirty(projectKey, validated.id);
627
523
 
628
- if (result.exitCode !== 0) {
524
+ return `Closed ${bead.id}: ${validated.reason}`;
525
+ } catch (error) {
526
+ const message = error instanceof Error ? error.message : String(error);
629
527
  throw new BeadError(
630
- `Failed to close bead because bd command failed: ${result.stderr}. Try: Verify bead exists and is not already closed with 'beads_query(status="closed")' or 'bd show ${validated.id}', check if bead ID is correct.`,
631
- cmd.join(" "),
632
- result.exitCode,
528
+ `Failed to close bead: ${message}`,
529
+ "beads_close",
633
530
  );
634
531
  }
635
-
636
- const bead = parseBead(result.stdout);
637
- return `Closed ${bead.id}: ${validated.reason}`;
638
532
  },
639
533
  });
640
534
 
@@ -648,24 +542,26 @@ export const beads_start = tool({
648
542
  id: tool.schema.string().describe("Bead ID"),
649
543
  },
650
544
  async execute(args, ctx) {
651
- const result = await runBdCommand([
652
- "update",
653
- args.id,
654
- "--status",
655
- "in_progress",
656
- "--json",
657
- ]);
545
+ const projectKey = getBeadsWorkingDirectory();
546
+ const adapter = await getBeadsAdapter(projectKey);
547
+
548
+ try {
549
+ const bead = await adapter.changeBeadStatus(
550
+ projectKey,
551
+ args.id,
552
+ "in_progress",
553
+ );
554
+
555
+ await adapter.markDirty(projectKey, args.id);
658
556
 
659
- if (result.exitCode !== 0) {
557
+ return `Started: ${bead.id}`;
558
+ } catch (error) {
559
+ const message = error instanceof Error ? error.message : String(error);
660
560
  throw new BeadError(
661
- `Failed to start bead because bd update command failed: ${result.stderr}. Try: Verify bead exists with 'bd show ${args.id}', check if already in_progress with 'beads_query(status="in_progress")', or use beads_update directly.`,
662
- `bd update ${args.id} --status in_progress --json`,
663
- result.exitCode,
561
+ `Failed to start bead: ${message}`,
562
+ "beads_start",
664
563
  );
665
564
  }
666
-
667
- const bead = parseBead(result.stdout);
668
- return `Started: ${bead.id}`;
669
565
  },
670
566
  });
671
567
 
@@ -676,24 +572,25 @@ export const beads_ready = tool({
676
572
  description: "Get the next ready bead (unblocked, highest priority)",
677
573
  args: {},
678
574
  async execute(args, ctx) {
679
- const result = await runBdCommand(["ready", "--json"]);
575
+ const projectKey = getBeadsWorkingDirectory();
576
+ const adapter = await getBeadsAdapter(projectKey);
680
577
 
681
- if (result.exitCode !== 0) {
682
- throw new BeadError(
683
- `Failed to get ready beads because bd ready command failed: ${result.stderr}. Try: Check if beads initialized with 'bd init', verify .beads/ directory is readable, or run 'bd list --json' to test basic query.`,
684
- "bd ready --json",
685
- result.exitCode,
686
- );
687
- }
578
+ try {
579
+ const bead = await adapter.getNextReadyBead(projectKey);
688
580
 
689
- const beads = parseBeads(result.stdout);
581
+ if (!bead) {
582
+ return "No ready beads";
583
+ }
690
584
 
691
- if (beads.length === 0) {
692
- return "No ready beads";
585
+ const formatted = formatBeadForOutput(bead);
586
+ return JSON.stringify(formatted, null, 2);
587
+ } catch (error) {
588
+ const message = error instanceof Error ? error.message : String(error);
589
+ throw new BeadError(
590
+ `Failed to get ready beads: ${message}`,
591
+ "beads_ready",
592
+ );
693
593
  }
694
-
695
- const next = beads[0];
696
- return JSON.stringify(next, null, 2);
697
594
  },
698
595
  });
699
596
 
@@ -710,11 +607,12 @@ export const beads_sync = tool({
710
607
  },
711
608
  async execute(args, ctx) {
712
609
  const autoPull = args.auto_pull ?? true;
610
+ const projectKey = getBeadsWorkingDirectory();
611
+ const adapter = await getBeadsAdapter(projectKey);
713
612
  const TIMEOUT_MS = 30000; // 30 seconds
714
613
 
715
614
  /**
716
615
  * Helper to run a command with timeout
717
- * Properly clears the timeout to avoid lingering timers
718
616
  */
719
617
  const withTimeout = async <T>(
720
618
  promise: Promise<T>,
@@ -745,18 +643,21 @@ export const beads_sync = tool({
745
643
  }
746
644
  };
747
645
 
748
- // 1. Flush beads to JSONL (doesn't use worktrees)
646
+ // 1. Flush beads to JSONL using FlushManager
647
+ const flushManager = new FlushManager({
648
+ adapter,
649
+ projectKey,
650
+ outputPath: `${projectKey}/.beads/issues.jsonl`,
651
+ });
652
+
749
653
  const flushResult = await withTimeout(
750
- runBdCommand(["sync", "--flush-only"]),
654
+ flushManager.flush(),
751
655
  TIMEOUT_MS,
752
- "bd sync --flush-only",
656
+ "flush beads",
753
657
  );
754
- if (flushResult.exitCode !== 0) {
755
- throw new BeadError(
756
- `Failed to flush beads because bd sync failed: ${flushResult.stderr}. Try: Check if .beads/ directory is writable, verify no corrupted JSONL files, or run 'bd list' to test basic beads functionality.`,
757
- "bd sync --flush-only",
758
- flushResult.exitCode,
759
- );
658
+
659
+ if (flushResult.beadsExported === 0) {
660
+ return "No beads to sync";
760
661
  }
761
662
 
762
663
  // 2. Check if there are changes to commit
@@ -772,7 +673,7 @@ export const beads_sync = tool({
772
673
  const addResult = await runGitCommand(["add", ".beads/"]);
773
674
  if (addResult.exitCode !== 0) {
774
675
  throw new BeadError(
775
- `Failed to stage beads because git add failed: ${addResult.stderr}. Try: Check if .beads/ directory exists, verify git is initialized with 'git status', or check for .gitignore patterns blocking .beads/.`,
676
+ `Failed to stage beads: ${addResult.stderr}`,
776
677
  "git add .beads/",
777
678
  addResult.exitCode,
778
679
  );
@@ -789,91 +690,31 @@ export const beads_sync = tool({
789
690
  !commitResult.stdout.includes("nothing to commit")
790
691
  ) {
791
692
  throw new BeadError(
792
- `Failed to commit beads because git commit failed: ${commitResult.stderr}. Try: Check git config (user.name, user.email) with 'git config --list', verify working tree is clean, or check for pre-commit hooks blocking commit.`,
693
+ `Failed to commit beads: ${commitResult.stderr}`,
793
694
  "git commit",
794
695
  commitResult.exitCode,
795
696
  );
796
697
  }
797
698
  }
798
699
 
799
- // 5. Pull if requested (with rebase to avoid merge commits)
700
+ // 5. Pull if requested
800
701
  if (autoPull) {
801
- // Check for unstaged changes that would block pull --rebase
802
- const dirtyCheckResult = await runGitCommand([
803
- "status",
804
- "--porcelain",
805
- "--untracked-files=no",
806
- ]);
807
- const hasDirtyFiles = dirtyCheckResult.stdout.trim() !== "";
808
- let didStash = false;
809
-
810
- // Stash dirty files before pull (self-healing for "unstaged changes" error)
811
- if (hasDirtyFiles) {
812
- console.warn(
813
- "[beads] Detected unstaged changes, stashing before pull...",
814
- );
815
- const stashResult = await runGitCommand([
816
- "stash",
817
- "push",
818
- "-m",
819
- "beads_sync: auto-stash before pull",
820
- "--include-untracked",
821
- ]);
822
- if (stashResult.exitCode === 0) {
823
- didStash = true;
824
- console.warn("[beads] Changes stashed successfully");
825
- } else {
826
- // Stash failed - try pull anyway, it might work
827
- console.warn(
828
- `[beads] Stash failed (${stashResult.stderr}), attempting pull anyway...`,
829
- );
830
- }
831
- }
832
-
833
702
  const pullResult = await withTimeout(
834
703
  runGitCommand(["pull", "--rebase"]),
835
704
  TIMEOUT_MS,
836
705
  "git pull --rebase",
837
706
  );
838
707
 
839
- // Restore stashed changes regardless of pull result
840
- if (didStash) {
841
- console.warn("[beads] Restoring stashed changes...");
842
- const unstashResult = await runGitCommand(["stash", "pop"]);
843
- if (unstashResult.exitCode !== 0) {
844
- // Unstash failed - this is bad, user needs to know
845
- console.error(
846
- `[beads] WARNING: Failed to restore stashed changes: ${unstashResult.stderr}`,
847
- );
848
- console.error(
849
- "[beads] Your changes are in 'git stash list' - run 'git stash pop' manually",
850
- );
851
- } else {
852
- console.warn("[beads] Stashed changes restored");
853
- }
854
- }
855
-
856
708
  if (pullResult.exitCode !== 0) {
857
709
  throw new BeadError(
858
- `Failed to pull because git pull --rebase failed: ${pullResult.stderr}. Try: Resolve merge conflicts manually with 'git status', check if remote is accessible with 'git remote -v', or use skip_verification to bypass automatic pull.`,
710
+ `Failed to pull: ${pullResult.stderr}`,
859
711
  "git pull --rebase",
860
712
  pullResult.exitCode,
861
713
  );
862
714
  }
863
-
864
- // 6. Import any changes from remote
865
- const importResult = await withTimeout(
866
- runBdCommand(["sync", "--import-only"]),
867
- TIMEOUT_MS,
868
- "bd sync --import-only",
869
- );
870
- if (importResult.exitCode !== 0) {
871
- // Non-fatal - just log warning
872
- console.warn(`[beads] Import warning: ${importResult.stderr}`);
873
- }
874
715
  }
875
716
 
876
- // 7. Push
717
+ // 6. Push
877
718
  const pushResult = await withTimeout(
878
719
  runGitCommand(["push"]),
879
720
  TIMEOUT_MS,
@@ -881,20 +722,12 @@ export const beads_sync = tool({
881
722
  );
882
723
  if (pushResult.exitCode !== 0) {
883
724
  throw new BeadError(
884
- `Failed to push because git push failed: ${pushResult.stderr}. Try: Check if remote branch is up to date with 'git pull --rebase', verify push permissions, check remote URL with 'git remote -v', or force push with 'git push --force-with-lease' if safe.`,
725
+ `Failed to push: ${pushResult.stderr}`,
885
726
  "git push",
886
727
  pushResult.exitCode,
887
728
  );
888
729
  }
889
730
 
890
- // 4. Verify clean state
891
- const statusResult = await runGitCommand(["status", "--porcelain"]);
892
- const status = statusResult.stdout.trim();
893
-
894
- if (status !== "") {
895
- return `Beads synced and pushed, but working directory not clean:\n${status}`;
896
- }
897
-
898
731
  return "Beads synced and pushed successfully";
899
732
  },
900
733
  });
@@ -909,48 +742,44 @@ export const beads_link_thread = tool({
909
742
  thread_id: tool.schema.string().describe("Agent Mail thread ID"),
910
743
  },
911
744
  async execute(args, ctx) {
912
- // Update bead description to include thread link
913
- // This is a workaround since bd doesn't have native metadata support
914
- const queryResult = await runBdCommand(["show", args.bead_id, "--json"]);
745
+ const projectKey = getBeadsWorkingDirectory();
746
+ const adapter = await getBeadsAdapter(projectKey);
915
747
 
916
- if (queryResult.exitCode !== 0) {
917
- throw new BeadError(
918
- `Failed to get bead because bd show command failed: ${queryResult.stderr}. Try: Verify bead ID is correct with 'beads_query()', check if bead exists with 'bd list --json', or check .beads/issues.jsonl for valid entries.`,
919
- `bd show ${args.bead_id} --json`,
920
- queryResult.exitCode,
921
- );
922
- }
748
+ try {
749
+ const bead = await adapter.getBead(projectKey, args.bead_id);
750
+
751
+ if (!bead) {
752
+ throw new BeadError(
753
+ `Bead not found: ${args.bead_id}`,
754
+ "beads_link_thread",
755
+ );
756
+ }
923
757
 
924
- const bead = parseBead(queryResult.stdout);
925
- const existingDesc = bead.description || "";
758
+ const existingDesc = bead.description || "";
759
+ const threadMarker = `[thread:${args.thread_id}]`;
926
760
 
927
- // Add thread link if not already present
928
- const threadMarker = `[thread:${args.thread_id}]`;
929
- if (existingDesc.includes(threadMarker)) {
930
- return `Bead ${args.bead_id} already linked to thread ${args.thread_id}`;
931
- }
761
+ if (existingDesc.includes(threadMarker)) {
762
+ return `Bead ${args.bead_id} already linked to thread ${args.thread_id}`;
763
+ }
932
764
 
933
- const newDesc = existingDesc
934
- ? `${existingDesc}\n\n${threadMarker}`
935
- : threadMarker;
765
+ const newDesc = existingDesc
766
+ ? `${existingDesc}\n\n${threadMarker}`
767
+ : threadMarker;
936
768
 
937
- const updateResult = await runBdCommand([
938
- "update",
939
- args.bead_id,
940
- "-d",
941
- newDesc,
942
- "--json",
943
- ]);
769
+ await adapter.updateBead(projectKey, args.bead_id, {
770
+ description: newDesc,
771
+ });
772
+
773
+ await adapter.markDirty(projectKey, args.bead_id);
944
774
 
945
- if (updateResult.exitCode !== 0) {
775
+ return `Linked bead ${args.bead_id} to thread ${args.thread_id}`;
776
+ } catch (error) {
777
+ const message = error instanceof Error ? error.message : String(error);
946
778
  throw new BeadError(
947
- `Failed to update bead because bd update command failed: ${updateResult.stderr}. Try: Verify bead exists with 'bd show ${args.bead_id}', check for invalid characters in description, or inspect .beads/issues.jsonl for corruption.`,
948
- `bd update ${args.bead_id} -d ...`,
949
- updateResult.exitCode,
779
+ `Failed to link thread: ${message}`,
780
+ "beads_link_thread",
950
781
  );
951
782
  }
952
-
953
- return `Linked bead ${args.bead_id} to thread ${args.thread_id}`;
954
783
  },
955
784
  });
956
785