opencode-swarm-plugin 0.25.2 → 0.26.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.
- package/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +48 -0
- package/dist/beads.d.ts +6 -0
- package/dist/beads.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +192 -287
- package/dist/plugin.js +191 -292
- package/dist/rate-limiter.d.ts.map +1 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/docs/swarm-mail-architecture.md +1 -1
- package/package.json +2 -2
- package/src/beads.integration.test.ts +55 -61
- package/src/beads.ts +239 -410
- package/src/index.ts +1 -15
- package/src/rate-limiter.ts +0 -5
- package/src/storage.ts +0 -9
- package/src/swarm-mail.integration.test.ts +5 -1
- package/src/swarm-orchestrate.ts +24 -14
- package/src/swarm.integration.test.ts +83 -0
- package/src/agent-mail.integration.test.ts +0 -1429
package/src/beads.ts
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Beads Module - Type-safe wrappers
|
|
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
|
|
5
|
+
* issue tracker using the BeadsAdapter from swarm-mail.
|
|
6
6
|
*
|
|
7
7
|
* Key principles:
|
|
8
|
-
* -
|
|
9
|
-
* - Validate all
|
|
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
|
|
11
|
+
* - Support atomic epic creation with rollback
|
|
12
12
|
*
|
|
13
13
|
* IMPORTANT: Call setBeadsWorkingDirectory() before using tools to ensure
|
|
14
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
139
|
+
const swarmMail = await getSwarmMail(projectKey);
|
|
140
|
+
const db = await swarmMail.getDatabase();
|
|
141
|
+
const adapter = createBeadsAdapter(db, projectKey);
|
|
162
142
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
parts.push("--id", args.id);
|
|
166
|
-
}
|
|
143
|
+
// Run migrations to ensure schema exists
|
|
144
|
+
await adapter.runMigrations();
|
|
167
145
|
|
|
168
|
-
|
|
169
|
-
return
|
|
146
|
+
adapterCache.set(projectKey, adapter);
|
|
147
|
+
return adapter;
|
|
170
148
|
}
|
|
171
149
|
|
|
172
150
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
203
|
+
const projectKey = getBeadsWorkingDirectory();
|
|
204
|
+
const adapter = await getBeadsAdapter(projectKey);
|
|
256
205
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
-
`
|
|
284
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 -
|
|
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
|
-
|
|
446
|
-
"
|
|
447
|
-
|
|
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}
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
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
|
|
543
|
-
|
|
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
|
-
|
|
457
|
+
try {
|
|
458
|
+
let bead: AdapterBead;
|
|
578
459
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
487
|
+
await adapter.markDirty(projectKey, validated.id);
|
|
591
488
|
|
|
592
|
-
|
|
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
|
|
595
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
"--json",
|
|
624
|
-
];
|
|
515
|
+
try {
|
|
516
|
+
const bead = await adapter.closeBead(
|
|
517
|
+
projectKey,
|
|
518
|
+
validated.id,
|
|
519
|
+
validated.reason,
|
|
520
|
+
);
|
|
625
521
|
|
|
626
|
-
|
|
522
|
+
await adapter.markDirty(projectKey, validated.id);
|
|
627
523
|
|
|
628
|
-
|
|
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
|
|
631
|
-
|
|
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
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
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
|
|
662
|
-
|
|
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
|
|
575
|
+
const projectKey = getBeadsWorkingDirectory();
|
|
576
|
+
const adapter = await getBeadsAdapter(projectKey);
|
|
680
577
|
|
|
681
|
-
|
|
682
|
-
|
|
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
|
-
|
|
581
|
+
if (!bead) {
|
|
582
|
+
return "No ready beads";
|
|
583
|
+
}
|
|
690
584
|
|
|
691
|
-
|
|
692
|
-
return
|
|
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
|
|
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
|
-
|
|
654
|
+
flushManager.flush(),
|
|
751
655
|
TIMEOUT_MS,
|
|
752
|
-
"
|
|
656
|
+
"flush beads",
|
|
753
657
|
);
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
const queryResult = await runBdCommand(["show", args.bead_id, "--json"]);
|
|
745
|
+
const projectKey = getBeadsWorkingDirectory();
|
|
746
|
+
const adapter = await getBeadsAdapter(projectKey);
|
|
915
747
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
925
|
-
|
|
758
|
+
const existingDesc = bead.description || "";
|
|
759
|
+
const threadMarker = `[thread:${args.thread_id}]`;
|
|
926
760
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
-
|
|
934
|
-
|
|
935
|
-
|
|
765
|
+
const newDesc = existingDesc
|
|
766
|
+
? `${existingDesc}\n\n${threadMarker}`
|
|
767
|
+
: threadMarker;
|
|
936
768
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
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
|
|
948
|
-
|
|
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
|
|