opencodekit 0.11.1 → 0.12.1

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.
@@ -1,5 +1,10 @@
1
+ import { createHash } from "crypto";
1
2
  import { type Plugin, tool } from "@opencode-ai/plugin";
2
3
 
4
+ // =============================================================================
5
+ // Types
6
+ // =============================================================================
7
+
3
8
  interface TaskState {
4
9
  currentTask: string | null;
5
10
  reservedFiles: Set<string>;
@@ -9,63 +14,580 @@ interface TaskState {
9
14
  agentId: string;
10
15
  }
11
16
 
17
+ interface Task {
18
+ id: string;
19
+ title: string;
20
+ status?: string;
21
+ priority?: number;
22
+ type?: string;
23
+ tags?: string[];
24
+ description?: string;
25
+ }
26
+
27
+ interface LockData {
28
+ path: string;
29
+ agent: string;
30
+ reason?: string;
31
+ created: number;
32
+ expires: number;
33
+ task: string | null;
34
+ }
35
+
36
+ interface Message {
37
+ id: string;
38
+ from: string;
39
+ to: string;
40
+ subj: string;
41
+ body?: string;
42
+ importance: string;
43
+ thread?: string;
44
+ global?: boolean;
45
+ at: number;
46
+ read: boolean;
47
+ }
48
+
49
+ interface BdListResult {
50
+ tasks: Task[];
51
+ error?: string;
52
+ }
53
+
54
+ interface BdSingleResult {
55
+ task?: Task;
56
+ id?: string;
57
+ output?: string;
58
+ error?: string;
59
+ }
60
+
61
+ interface BdGenericResult {
62
+ output?: string;
63
+ error?: string;
64
+ }
65
+
66
+ // =============================================================================
67
+ // Plugin
68
+ // =============================================================================
69
+
12
70
  export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
71
+ // Generate deterministic agent ID from directory + process
72
+ const agentId = `agent-${createHash("md5")
73
+ .update(`${directory}-${process.pid}`)
74
+ .digest("hex")
75
+ .slice(0, 8)}`;
76
+
13
77
  const state: TaskState = {
14
78
  currentTask: null,
15
79
  reservedFiles: new Set(),
16
80
  team: "default",
17
81
  role: "",
18
82
  initialized: false,
19
- agentId: `agent-${Date.now().toString(36)}`,
83
+ agentId,
20
84
  };
21
85
 
22
- async function bd(...args: string[]): Promise<any> {
86
+ const RESERVATIONS_DIR = ".reservations";
87
+
88
+ // =============================================================================
89
+ // Shell Helpers
90
+ // =============================================================================
91
+
92
+ // (Removed unused shell/shellQuiet helpers)
93
+
94
+ // =============================================================================
95
+ // Typed BD CLI Wrappers
96
+ // =============================================================================
97
+
98
+ async function bdList(
99
+ opts: {
100
+ status?: string;
101
+ sort?: string;
102
+ reverse?: boolean;
103
+ label?: string;
104
+ assignee?: string;
105
+ type?: string;
106
+ priorityMin?: number;
107
+ priorityMax?: number;
108
+ limit?: number;
109
+ } = {},
110
+ ): Promise<{ tasks: Task[]; error?: string }> {
111
+ try {
112
+ const args = ["list", "--json"];
113
+ if (opts.status) args.push("--status", opts.status);
114
+ if (opts.sort) args.push("--sort", opts.sort);
115
+ if (opts.reverse) args.push("--reverse");
116
+ if (opts.label) args.push("--label", opts.label);
117
+ if (opts.assignee) args.push("--assignee", opts.assignee);
118
+ if (opts.type) args.push("--type", opts.type);
119
+ if (opts.priorityMin !== undefined)
120
+ args.push("--priority-min", String(opts.priorityMin));
121
+ if (opts.priorityMax !== undefined)
122
+ args.push("--priority-max", String(opts.priorityMax));
123
+ if (opts.limit) args.push("--limit", String(opts.limit));
124
+ const result = await $`bd ${args}`.cwd(directory).text();
125
+ const parsed = JSON.parse(result);
126
+ return { tasks: Array.isArray(parsed) ? parsed : [] };
127
+ } catch (e) {
128
+ return { tasks: [], error: e instanceof Error ? e.message : String(e) };
129
+ }
130
+ }
131
+
132
+ async function bdReady(
133
+ opts: {
134
+ sort?: string;
135
+ limit?: number;
136
+ assignee?: string;
137
+ label?: string;
138
+ unassigned?: boolean;
139
+ } = {},
140
+ ): Promise<{ tasks: Task[]; error?: string }> {
141
+ try {
142
+ const args = ["ready", "--json"];
143
+ if (opts.sort) args.push("--sort", opts.sort);
144
+ if (opts.limit) args.push("--limit", String(opts.limit));
145
+ if (opts.assignee) args.push("--assignee", opts.assignee);
146
+ if (opts.label) args.push("--label", opts.label);
147
+ if (opts.unassigned) args.push("--unassigned");
148
+ const result = await $`bd ${args}`.cwd(directory).text();
149
+ const parsed = JSON.parse(result);
150
+ return { tasks: Array.isArray(parsed) ? parsed : [] };
151
+ } catch (e) {
152
+ return { tasks: [], error: e instanceof Error ? e.message : String(e) };
153
+ }
154
+ }
155
+
156
+ async function bdShow(id: string): Promise<{ task?: Task; error?: string }> {
157
+ try {
158
+ const result = await $`bd show ${id} --json`.cwd(directory).text();
159
+ return { task: JSON.parse(result) };
160
+ } catch (e) {
161
+ return { error: e instanceof Error ? e.message : String(e) };
162
+ }
163
+ }
164
+
165
+ async function bdCreate(
166
+ title: string,
167
+ opts: {
168
+ priority?: number;
169
+ type?: string;
170
+ description?: string;
171
+ parent?: string;
172
+ tags?: string[];
173
+ deps?: string[];
174
+ assignee?: string;
175
+ estimate?: number; // minutes
176
+ acceptance?: string;
177
+ } = {},
178
+ ): Promise<{ id?: string; error?: string }> {
179
+ try {
180
+ const args = ["create", title, "-p", String(opts.priority ?? 2)];
181
+ if (opts.type && opts.type !== "task") args.push("--type", opts.type);
182
+ if (opts.description) args.push("--description", opts.description);
183
+ if (opts.parent) args.push("--parent", opts.parent);
184
+ if (opts.tags?.length) args.push("--labels", opts.tags.join(","));
185
+ if (opts.deps?.length) args.push("--deps", opts.deps.join(","));
186
+ if (opts.assignee) args.push("--assignee", opts.assignee);
187
+ if (opts.estimate !== undefined)
188
+ args.push("--estimate", String(opts.estimate));
189
+ if (opts.acceptance) args.push("--acceptance", opts.acceptance);
190
+ args.push("--json");
191
+
192
+ const result = await $`bd ${args}`.cwd(directory).text();
193
+ // bd create may output warnings before JSON, extract JSON
194
+ const jsonMatch = result.match(/\{[\s\S]*\}/);
195
+ if (jsonMatch) {
196
+ const parsed = JSON.parse(jsonMatch[0]);
197
+ return { id: parsed.id };
198
+ }
199
+ return { id: result.trim() };
200
+ } catch (e) {
201
+ return { error: e instanceof Error ? e.message : String(e) };
202
+ }
203
+ }
204
+
205
+ async function bdUpdate(
206
+ id: string,
207
+ opts: {
208
+ status?: string;
209
+ addLabel?: string;
210
+ removeLabel?: string;
211
+ setLabels?: string;
212
+ title?: string;
213
+ description?: string;
214
+ notes?: string;
215
+ priority?: number;
216
+ assignee?: string;
217
+ estimate?: string;
218
+ removeDep?: string;
219
+ addDep?: string;
220
+ },
221
+ ): Promise<{ error?: string }> {
222
+ try {
223
+ const args = ["update", id];
224
+ if (opts.status) args.push("--status", opts.status);
225
+ if (opts.addLabel) args.push("--add-label", opts.addLabel);
226
+ if (opts.removeLabel) args.push("--remove-label", opts.removeLabel);
227
+ if (opts.setLabels) args.push("--set-labels", opts.setLabels);
228
+ if (opts.title) args.push("--title", opts.title);
229
+ if (opts.description) args.push("--description", opts.description);
230
+ if (opts.notes) args.push("--notes", opts.notes);
231
+ if (opts.priority !== undefined)
232
+ args.push("--priority", String(opts.priority));
233
+ if (opts.assignee) args.push("--assignee", opts.assignee);
234
+ if (opts.estimate) args.push("--estimate", opts.estimate);
235
+ if (opts.removeDep) args.push("--remove-dep", opts.removeDep);
236
+ if (opts.addDep) args.push("--add-dep", opts.addDep);
237
+ await $`bd ${args}`.cwd(directory).quiet();
238
+ return {};
239
+ } catch (e) {
240
+ return { error: e instanceof Error ? e.message : String(e) };
241
+ }
242
+ }
243
+
244
+ async function bdClose(
245
+ id: string,
246
+ reason: string,
247
+ ): Promise<{ error?: string }> {
248
+ try {
249
+ await $`bd close ${id} --reason ${reason}`.cwd(directory).quiet();
250
+ return {};
251
+ } catch (e) {
252
+ return { error: e instanceof Error ? e.message : String(e) };
253
+ }
254
+ }
255
+
256
+ async function bdReopen(id: string): Promise<{ error?: string }> {
257
+ try {
258
+ await $`bd reopen ${id}`.cwd(directory).quiet();
259
+ return {};
260
+ } catch (e) {
261
+ return { error: e instanceof Error ? e.message : String(e) };
262
+ }
263
+ }
264
+
265
+ async function bdSearch(
266
+ query: string,
267
+ ): Promise<{ tasks: Task[]; error?: string }> {
268
+ try {
269
+ const result = await $`bd search ${query} --json`.cwd(directory).text();
270
+ const parsed = JSON.parse(result);
271
+ return { tasks: Array.isArray(parsed) ? parsed : [] };
272
+ } catch (e) {
273
+ return { tasks: [], error: e instanceof Error ? e.message : String(e) };
274
+ }
275
+ }
276
+
277
+ async function bdDepAdd(
278
+ child: string,
279
+ parent: string,
280
+ type = "blocks",
281
+ ): Promise<{ error?: string }> {
282
+ try {
283
+ await $`bd dep add ${child} ${parent} --type ${type}`
284
+ .cwd(directory)
285
+ .quiet();
286
+ return {};
287
+ } catch (e) {
288
+ return { error: e instanceof Error ? e.message : String(e) };
289
+ }
290
+ }
291
+
292
+ async function bdDepRemove(
293
+ child: string,
294
+ parent: string,
295
+ ): Promise<{ error?: string }> {
296
+ try {
297
+ await $`bd dep remove ${child} ${parent}`.cwd(directory).quiet();
298
+ return {};
299
+ } catch (e) {
300
+ return { error: e instanceof Error ? e.message : String(e) };
301
+ }
302
+ }
303
+
304
+ async function bdDepTree(
305
+ id: string,
306
+ ): Promise<{ output?: string; error?: string }> {
23
307
  try {
24
- const result = await $`bd ${args}`.text();
308
+ const result = await $`bd dep tree ${id}`.cwd(directory).text();
309
+ return { output: result.trim() };
310
+ } catch (e) {
311
+ return { error: e instanceof Error ? e.message : String(e) };
312
+ }
313
+ }
314
+
315
+ async function bdSync(): Promise<{ output?: string; error?: string }> {
316
+ try {
317
+ const result = await $`bd sync`.cwd(directory).text();
318
+ return { output: result.trim() };
319
+ } catch (e) {
320
+ return { error: e instanceof Error ? e.message : String(e) };
321
+ }
322
+ }
323
+
324
+ async function bdDoctor(): Promise<{ output?: string; error?: string }> {
325
+ try {
326
+ const result = await $`bd doctor`.cwd(directory).text();
327
+ return { output: result.trim() };
328
+ } catch (e) {
329
+ return { error: e instanceof Error ? e.message : String(e) };
330
+ }
331
+ }
332
+
333
+ async function bdCleanup(
334
+ days: number,
335
+ ): Promise<{ output?: string; error?: string }> {
336
+ try {
337
+ const result = await $`bd cleanup --older-than ${days} --force`
338
+ .cwd(directory)
339
+ .text();
340
+ return { output: result.trim() };
341
+ } catch (e) {
342
+ return { error: e instanceof Error ? e.message : String(e) };
343
+ }
344
+ }
345
+
346
+ // =============================================================================
347
+ // File Locking (Atomic mkdir-based)
348
+ // =============================================================================
349
+
350
+ async function bdBlocked(): Promise<{ tasks: Task[]; error?: string }> {
351
+ try {
352
+ const result = await $`bd blocked --json`.cwd(directory).text();
353
+ const parsed = JSON.parse(result);
354
+ return { tasks: Array.isArray(parsed) ? parsed : [] };
355
+ } catch (e) {
356
+ return { tasks: [], error: e instanceof Error ? e.message : String(e) };
357
+ }
358
+ }
359
+
360
+ function lockDir(filePath: string): string {
361
+ const safe = filePath.replace(/[/\\]/g, "_").replace(/\.\./g, "_");
362
+ return `${RESERVATIONS_DIR}/${safe}.lock`;
363
+ }
364
+
365
+ async function acquireLock(
366
+ filePath: string,
367
+ reason?: string,
368
+ ttlSeconds = 600,
369
+ ): Promise<{ acquired: boolean; holder?: string }> {
370
+ const lockPath = lockDir(filePath);
371
+ const now = Date.now();
372
+ const expires = now + ttlSeconds * 1000;
373
+
374
+ // Atomic: mkdir fails if dir exists
375
+ try {
376
+ await $`mkdir ${lockPath}`.cwd(directory);
377
+ } catch {
378
+ // Lock exists - check if expired
25
379
  try {
26
- return JSON.parse(result);
380
+ const metaPath = `${lockPath}/meta.json`;
381
+ const content = await $`cat ${metaPath}`.cwd(directory).text();
382
+ const lock: LockData = JSON.parse(content);
383
+
384
+ if (lock.expires < now) {
385
+ // Expired - remove and retry
386
+ await $`rm -rf ${lockPath}`.cwd(directory).quiet();
387
+ return acquireLock(filePath, reason, ttlSeconds);
388
+ }
389
+
390
+ if (lock.agent === state.agentId) {
391
+ // We already hold this lock - refresh it
392
+ lock.expires = expires;
393
+ await $`echo ${JSON.stringify(lock)} > ${metaPath}`
394
+ .cwd(directory)
395
+ .quiet();
396
+ return { acquired: true };
397
+ }
398
+
399
+ return { acquired: false, holder: lock.agent };
27
400
  } catch {
28
- return { output: result.trim() };
401
+ // Corrupted lock - remove and retry
402
+ await $`rm -rf ${lockPath}`.cwd(directory).quiet();
403
+ return acquireLock(filePath, reason, ttlSeconds);
29
404
  }
30
- } catch (e: any) {
31
- return { error: e.message || String(e) };
32
405
  }
33
- }
34
406
 
35
- function j(data: any): string {
36
- return JSON.stringify(data, null, 0);
407
+ // Lock acquired - write metadata
408
+ const lockData: LockData = {
409
+ path: filePath,
410
+ agent: state.agentId,
411
+ reason,
412
+ created: now,
413
+ expires,
414
+ task: state.currentTask,
415
+ };
416
+
417
+ const metaPath = `${lockPath}/meta.json`;
418
+ await $`echo ${JSON.stringify(lockData)} > ${metaPath}`
419
+ .cwd(directory)
420
+ .quiet();
421
+ state.reservedFiles.add(filePath);
422
+ return { acquired: true };
37
423
  }
38
424
 
39
- function reservationPath(filePath: string): string {
40
- const safe = filePath.replace(/[\/\\]/g, "_").replace(/\.\./g, "_");
41
- return `.reservations/${safe}.lock`;
425
+ async function releaseLock(filePath: string): Promise<void> {
426
+ const lockPath = lockDir(filePath);
427
+ await $`rm -rf ${lockPath}`
428
+ .cwd(directory)
429
+ .quiet()
430
+ .catch(() => {});
431
+ state.reservedFiles.delete(filePath);
42
432
  }
43
433
 
44
- async function getAllReservations(): Promise<any[]> {
434
+ async function getAllLocks(): Promise<LockData[]> {
45
435
  try {
46
- const dir = ".reservations";
47
- const result = await $`test -d ${dir} && ls -1 ${dir}`
48
- .text()
49
- .catch(() => "");
436
+ const result =
437
+ await $`find ${RESERVATIONS_DIR} -name "meta.json" -type f 2>/dev/null`
438
+ .cwd(directory)
439
+ .text()
440
+ .catch(() => "");
441
+
50
442
  if (!result.trim()) return [];
51
443
 
52
- const reservations: any[] = [];
53
- for (const file of result.trim().split("\n")) {
54
- if (!file.endsWith(".lock")) continue;
444
+ const locks: LockData[] = [];
445
+ const now = Date.now();
446
+
447
+ for (const metaPath of result.trim().split("\n")) {
55
448
  try {
56
- const content = await $`cat "${dir}/${file}"`.text();
57
- const lock = JSON.parse(content);
58
- if (lock.expires > Date.now()) {
59
- reservations.push(lock);
449
+ const content = await $`cat ${metaPath}`.cwd(directory).text();
450
+ const lock: LockData = JSON.parse(content);
451
+ if (lock.expires > now) {
452
+ locks.push(lock);
60
453
  }
61
- } catch {}
454
+ } catch {
455
+ // Skip invalid
456
+ }
62
457
  }
63
- return reservations;
458
+ return locks;
64
459
  } catch {
65
460
  return [];
66
461
  }
67
462
  }
68
463
 
464
+ async function cleanupExpiredLocks(): Promise<number> {
465
+ let cleaned = 0;
466
+ try {
467
+ const result =
468
+ await $`find ${RESERVATIONS_DIR} -name "meta.json" -type f 2>/dev/null`
469
+ .cwd(directory)
470
+ .text()
471
+ .catch(() => "");
472
+
473
+ if (!result.trim()) return 0;
474
+
475
+ const now = Date.now();
476
+ for (const metaPath of result.trim().split("\n")) {
477
+ try {
478
+ const content = await $`cat ${metaPath}`.cwd(directory).text();
479
+ const lock: LockData = JSON.parse(content);
480
+ if (lock.expires < now) {
481
+ const lockDir = metaPath.replace("/meta.json", "");
482
+ await $`rm -rf ${lockDir}`.cwd(directory).quiet();
483
+ cleaned++;
484
+ }
485
+ } catch {
486
+ // Remove corrupted lock
487
+ const lockDir = metaPath.replace("/meta.json", "");
488
+ await $`rm -rf ${lockDir}`.cwd(directory).quiet();
489
+ cleaned++;
490
+ }
491
+ }
492
+ } catch {
493
+ // Ignore
494
+ }
495
+ return cleaned;
496
+ }
497
+
498
+ // =============================================================================
499
+ // Message Helpers
500
+ // =============================================================================
501
+
502
+ async function ensureReservationsDir(): Promise<void> {
503
+ await $`mkdir -p ${RESERVATIONS_DIR}`.cwd(directory).quiet();
504
+ }
505
+
506
+ async function appendMessage(msg: Message): Promise<void> {
507
+ await ensureReservationsDir();
508
+ await $`echo ${JSON.stringify(msg)} >> ${RESERVATIONS_DIR}/messages.jsonl`
509
+ .cwd(directory)
510
+ .quiet();
511
+ }
512
+
513
+ async function readMessages(
514
+ limit: number,
515
+ unreadOnly: boolean,
516
+ ): Promise<Message[]> {
517
+ try {
518
+ const content =
519
+ await $`cat ${RESERVATIONS_DIR}/messages.jsonl 2>/dev/null`
520
+ .cwd(directory)
521
+ .text();
522
+
523
+ if (!content.trim()) return [];
524
+
525
+ let msgs: Message[] = content
526
+ .trim()
527
+ .split("\n")
528
+ .map((line) => {
529
+ try {
530
+ return JSON.parse(line) as Message;
531
+ } catch {
532
+ return null;
533
+ }
534
+ })
535
+ .filter((m): m is Message => m !== null)
536
+ .filter((m) => m.to === "all" || m.to === state.agentId);
537
+
538
+ if (unreadOnly) msgs = msgs.filter((m) => !m.read);
539
+ return msgs.slice(-limit).reverse();
540
+ } catch {
541
+ return [];
542
+ }
543
+ }
544
+
545
+ async function cleanupOldMessages(maxAgeDays: number): Promise<number> {
546
+ try {
547
+ const content =
548
+ await $`cat ${RESERVATIONS_DIR}/messages.jsonl 2>/dev/null`
549
+ .cwd(directory)
550
+ .text();
551
+
552
+ if (!content.trim()) return 0;
553
+
554
+ const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
555
+ const msgs = content
556
+ .trim()
557
+ .split("\n")
558
+ .map((line) => {
559
+ try {
560
+ return JSON.parse(line) as Message;
561
+ } catch {
562
+ return null;
563
+ }
564
+ })
565
+ .filter((m): m is Message => m !== null && m.at > cutoff);
566
+
567
+ const originalCount = content.trim().split("\n").length;
568
+ const newContent = msgs.map((m) => JSON.stringify(m)).join("\n");
569
+ await $`echo ${newContent} > ${RESERVATIONS_DIR}/messages.jsonl`
570
+ .cwd(directory)
571
+ .quiet();
572
+
573
+ return originalCount - msgs.length;
574
+ } catch {
575
+ return 0;
576
+ }
577
+ }
578
+
579
+ // =============================================================================
580
+ // JSON Response Helper
581
+ // =============================================================================
582
+
583
+ function json(data: Record<string, unknown>): string {
584
+ return JSON.stringify(data, null, 0);
585
+ }
586
+
587
+ // =============================================================================
588
+ // Tools
589
+ // =============================================================================
590
+
69
591
  return {
70
592
  tool: {
71
593
  bd_init: tool({
@@ -78,8 +600,11 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
78
600
  .describe("Role: fe|be|mobile|devops|qa"),
79
601
  },
80
602
  async execute(args) {
81
- await $`bd init`.quiet().catch(() => {});
82
- await $`mkdir -p .reservations`.quiet();
603
+ await $`bd init`
604
+ .cwd(directory)
605
+ .quiet()
606
+ .catch(() => {});
607
+ await ensureReservationsDir();
83
608
 
84
609
  state.team = args.team || "default";
85
610
  state.role = args.role || "";
@@ -87,12 +612,16 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
87
612
  state.currentTask = null;
88
613
  state.reservedFiles.clear();
89
614
 
90
- return j({
615
+ // Cleanup expired locks on init
616
+ const cleaned = await cleanupExpiredLocks();
617
+
618
+ return json({
91
619
  ok: 1,
92
620
  agent: state.agentId,
93
621
  ws: directory,
94
622
  team: state.team,
95
623
  role: state.role || undefined,
624
+ cleaned_locks: cleaned || undefined,
96
625
  });
97
626
  },
98
627
  }),
@@ -101,27 +630,28 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
101
630
  description: "Claim next ready task. Auto-syncs, marks in_progress.",
102
631
  args: {},
103
632
  async execute() {
104
- if (!state.initialized) return j({ error: "Call bd_init() first" });
633
+ if (!state.initialized)
634
+ return json({ error: "Call bd_init() first" });
105
635
 
106
- await $`bd sync`.quiet().catch(() => {});
107
- const ready = await bd("ready", "--json");
108
- if (ready.error) return j({ error: ready.error });
109
-
110
- const tasks = Array.isArray(ready) ? ready : [];
111
- if (!tasks.length) return j({ msg: "no ready tasks" });
636
+ await bdSync();
637
+ const { tasks, error } = await bdReady();
638
+ if (error) return json({ error });
639
+ if (!tasks.length) return json({ msg: "no ready tasks" });
112
640
 
113
641
  let task = tasks[0];
114
642
  if (state.role) {
115
- const roleTask = tasks.find((t: any) =>
116
- t.tags?.includes(state.role),
117
- );
643
+ const roleTask = tasks.find((t) => t.tags?.includes(state.role));
118
644
  if (roleTask) task = roleTask;
119
645
  }
120
646
 
121
- await $`bd update ${task.id} --status in_progress`.quiet();
647
+ const updateResult = await bdUpdate(task.id, {
648
+ status: "in_progress",
649
+ });
650
+ if (updateResult.error) return json({ error: updateResult.error });
651
+
122
652
  state.currentTask = task.id;
123
653
 
124
- return j({
654
+ return json({
125
655
  id: task.id,
126
656
  t: task.title,
127
657
  p: task.priority,
@@ -142,19 +672,20 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
142
672
  },
143
673
  async execute(args) {
144
674
  const taskId = args.id || state.currentTask;
145
- if (!taskId) return j({ error: "No task ID" });
675
+ if (!taskId) return json({ error: "No task ID" });
146
676
 
147
- await bd("close", taskId, "--reason", args.msg);
677
+ const closeResult = await bdClose(taskId, args.msg);
678
+ if (closeResult.error) return json({ error: closeResult.error });
148
679
 
680
+ // Release all locks
149
681
  for (const path of state.reservedFiles) {
150
- await $`rm -f ${reservationPath(path)}`.quiet().catch(() => {});
682
+ await releaseLock(path);
151
683
  }
152
- state.reservedFiles.clear();
153
684
 
154
- await $`bd sync`.quiet().catch(() => {});
685
+ await bdSync();
155
686
  state.currentTask = null;
156
687
 
157
- return j({ ok: 1, closed: taskId, hint: "Restart session" });
688
+ return json({ ok: 1, closed: taskId, hint: "Restart session" });
158
689
  },
159
690
  }),
160
691
 
@@ -183,22 +714,35 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
183
714
  .array(tool.schema.string())
184
715
  .optional()
185
716
  .describe("Dependencies (type:id format)"),
717
+ assignee: tool.schema.string().optional().describe("Assignee name"),
718
+ estimate: tool.schema
719
+ .number()
720
+ .optional()
721
+ .describe("Time estimate in minutes (e.g. 60 for 1h)"),
722
+ acceptance: tool.schema
723
+ .string()
724
+ .optional()
725
+ .describe("Acceptance criteria"),
186
726
  },
187
727
  async execute(args) {
188
- if (!args.title) return j({ error: "title required" });
189
-
190
- const cmdArgs = ["create", args.title, "-p", String(args.pri || 2)];
191
- if (args.type && args.type !== "task")
192
- cmdArgs.push("--type", args.type);
193
- if (args.desc) cmdArgs.push("--description", args.desc);
194
- if (args.parent) cmdArgs.push("--parent", args.parent);
195
- if (args.tags?.length) cmdArgs.push("--tags", args.tags.join(","));
196
- if (args.deps?.length) cmdArgs.push("--deps", args.deps.join(","));
197
- cmdArgs.push("--json");
198
-
199
- const result = await bd(...cmdArgs);
200
- return j({
201
- id: result.id || result.output,
728
+ if (!args.title) return json({ error: "title required" });
729
+
730
+ const result = await bdCreate(args.title, {
731
+ priority: args.pri,
732
+ type: args.type,
733
+ description: args.desc,
734
+ parent: args.parent,
735
+ tags: args.tags,
736
+ deps: args.deps,
737
+ assignee: args.assignee,
738
+ estimate: args.estimate,
739
+ acceptance: args.acceptance,
740
+ });
741
+
742
+ if (result.error) return json({ error: result.error });
743
+
744
+ return json({
745
+ id: result.id,
202
746
  t: args.title,
203
747
  p: args.pri || 2,
204
748
  });
@@ -218,21 +762,24 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
218
762
  },
219
763
  async execute(args) {
220
764
  if (!args.id || !args.role)
221
- return j({ error: "id and role required" });
765
+ return json({ error: "id and role required" });
222
766
 
223
- await bd("update", args.id, "--tags", args.role);
767
+ const result = await bdUpdate(args.id, { addLabel: args.role });
768
+ if (result.error) return json({ error: result.error });
224
769
 
225
770
  if (args.notify !== false) {
226
- const notif = {
227
- type: "assign",
228
- task: args.id,
229
- role: args.role,
771
+ await appendMessage({
772
+ id: `notify-${Date.now().toString(36)}`,
773
+ from: state.agentId,
774
+ to: "all",
775
+ subj: `Task ${args.id} assigned to ${args.role}`,
776
+ importance: "normal",
230
777
  at: Date.now(),
231
- };
232
- await $`echo ${JSON.stringify(notif)} >> .reservations/notifications.jsonl`.quiet();
778
+ read: false,
779
+ });
233
780
  }
234
781
 
235
- return j({ ok: 1, id: args.id, role: args.role });
782
+ return json({ ok: 1, id: args.id, role: args.role });
236
783
  },
237
784
  }),
238
785
 
@@ -242,25 +789,61 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
242
789
  status: tool.schema.string().default("open"),
243
790
  limit: tool.schema.number().default(10),
244
791
  offset: tool.schema.number().default(0),
792
+ sort: tool.schema
793
+ .string()
794
+ .optional()
795
+ .describe("Sort by: priority|created|updated|title"),
796
+ reverse: tool.schema
797
+ .boolean()
798
+ .optional()
799
+ .describe("Reverse sort order"),
800
+ label: tool.schema.string().optional().describe("Filter by label"),
801
+ assignee: tool.schema
802
+ .string()
803
+ .optional()
804
+ .describe("Filter by assignee"),
805
+ type: tool.schema
806
+ .string()
807
+ .optional()
808
+ .describe("Filter by type: task|bug|feature|epic"),
809
+ priorityMin: tool.schema
810
+ .number()
811
+ .optional()
812
+ .describe("Min priority (0-4)"),
813
+ priorityMax: tool.schema
814
+ .number()
815
+ .optional()
816
+ .describe("Max priority (0-4)"),
245
817
  },
246
818
  async execute(args) {
247
819
  const status = args.status || "open";
248
820
  const limit = Math.min(args.limit || 10, 50);
821
+ const offset = args.offset || 0;
249
822
 
250
- let result: any;
251
- if (status === "ready") {
252
- result = await bd("ready", "--json");
253
- } else if (status === "all") {
254
- result = await bd("list", "--json");
255
- } else {
256
- result = await bd("list", "--status", status, "--json");
257
- }
823
+ const opts = {
824
+ status: status === "all" ? undefined : status,
825
+ sort: args.sort,
826
+ reverse: args.reverse,
827
+ label: args.label,
828
+ assignee: args.assignee,
829
+ type: args.type,
830
+ priorityMin: args.priorityMin,
831
+ priorityMax: args.priorityMax,
832
+ };
258
833
 
259
- if (result.error) return j({ error: result.error });
834
+ const result =
835
+ status === "ready"
836
+ ? await bdReady({
837
+ sort: args.sort,
838
+ limit,
839
+ assignee: args.assignee,
840
+ label: args.label,
841
+ })
842
+ : await bdList(opts);
260
843
 
261
- const tasks = Array.isArray(result) ? result : [];
262
- const offset = args.offset || 0;
263
- const items = tasks.slice(offset, offset + limit).map((t: any) => ({
844
+ if (result.error) return json({ error: result.error });
845
+
846
+ const items = result.tasks.slice(offset, offset + limit).map((t) => ({
264
847
  id: t.id,
265
848
  t: t.title,
266
849
  p: t.priority,
@@ -268,7 +851,11 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
268
851
  tags: t.tags?.length ? t.tags : undefined,
269
852
  }));
270
853
 
271
- return j({ items, count: items.length, total: tasks.length });
854
+ return json({
855
+ items,
856
+ count: items.length,
857
+ total: result.tasks.length,
858
+ });
272
859
  },
273
860
  }),
274
861
 
@@ -276,9 +863,10 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
276
863
  description: "Get full issue details.",
277
864
  args: { id: tool.schema.string().describe("Issue ID") },
278
865
  async execute(args) {
279
- if (!args.id) return j({ error: "id required" });
280
- const result = await bd("show", args.id, "--json");
281
- return j(result);
866
+ if (!args.id) return json({ error: "id required" });
867
+ const result = await bdShow(args.id);
868
+ if (result.error) return json({ error: result.error });
869
+ return json(result.task as unknown as Record<string, unknown>);
282
870
  },
283
871
  }),
284
872
 
@@ -295,46 +883,25 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
295
883
  .describe("Seconds until expiry"),
296
884
  },
297
885
  async execute(args) {
298
- if (!args.paths?.length) return j({ error: "paths required" });
886
+ if (!args.paths?.length) return json({ error: "paths required" });
299
887
 
300
- await $`mkdir -p .reservations`.quiet();
888
+ await ensureReservationsDir();
301
889
 
302
890
  const granted: string[] = [];
303
891
  const conflicts: { path: string; holder?: string }[] = [];
304
- const now = Date.now();
305
- const expires = now + (args.ttl || 600) * 1000;
306
892
 
307
893
  for (const path of args.paths) {
308
- const lockFile = reservationPath(path);
309
-
310
- try {
311
- const content = await $`cat ${lockFile} 2>/dev/null`.text();
312
- if (content) {
313
- const lock = JSON.parse(content);
314
- if (lock.expires > now && lock.agent !== state.agentId) {
315
- conflicts.push({ path, holder: lock.agent });
316
- continue;
317
- }
318
- }
319
- } catch {}
320
-
321
- const lockData = JSON.stringify({
322
- path,
323
- agent: state.agentId,
324
- reason: args.reason,
325
- created: now,
326
- expires,
327
- task: state.currentTask,
328
- });
329
-
330
- await $`echo ${lockData} > ${lockFile}`.quiet();
331
- granted.push(path);
332
- state.reservedFiles.add(path);
894
+ const result = await acquireLock(path, args.reason, args.ttl);
895
+ if (result.acquired) {
896
+ granted.push(path);
897
+ } else {
898
+ conflicts.push({ path, holder: result.holder });
899
+ }
333
900
  }
334
901
 
335
- const result: any = { granted };
336
- if (conflicts.length) result.conflicts = conflicts;
337
- return j(result);
902
+ const response: Record<string, unknown> = { granted };
903
+ if (conflicts.length) response.conflicts = conflicts;
904
+ return json(response);
338
905
  },
339
906
  }),
340
907
 
@@ -352,11 +919,10 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
352
919
  : [...state.reservedFiles];
353
920
 
354
921
  for (const path of toRelease) {
355
- await $`rm -f ${reservationPath(path)}`.quiet().catch(() => {});
356
- state.reservedFiles.delete(path);
922
+ await releaseLock(path);
357
923
  }
358
924
 
359
- return j({ released: toRelease });
925
+ return json({ released: toRelease });
360
926
  },
361
927
  }),
362
928
 
@@ -364,15 +930,15 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
364
930
  description: "List active file locks. Check before editing.",
365
931
  args: {},
366
932
  async execute() {
367
- const reservations = await getAllReservations();
368
- return j({
369
- locks: reservations.map((r) => ({
933
+ const locks = await getAllLocks();
934
+ return json({
935
+ locks: locks.map((r) => ({
370
936
  path: r.path,
371
937
  agent: r.agent,
372
938
  expires: new Date(r.expires).toISOString(),
373
939
  task: r.task,
374
940
  })),
375
- count: reservations.length,
941
+ count: locks.length,
376
942
  });
377
943
  },
378
944
  }),
@@ -398,9 +964,9 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
398
964
  .describe("Send to all workspaces"),
399
965
  },
400
966
  async execute(args) {
401
- if (!args.subj) return j({ error: "subj required" });
967
+ if (!args.subj) return json({ error: "subj required" });
402
968
 
403
- const msg = {
969
+ const msg: Message = {
404
970
  id: `msg-${Date.now().toString(36)}`,
405
971
  from: state.agentId,
406
972
  to: args.to || "all",
@@ -413,10 +979,8 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
413
979
  read: false,
414
980
  };
415
981
 
416
- await $`mkdir -p .reservations`.quiet();
417
- await $`echo ${JSON.stringify(msg)} >> .reservations/messages.jsonl`.quiet();
418
-
419
- return j({ ok: 1, id: msg.id });
982
+ await appendMessage(msg);
983
+ return json({ ok: 1, id: msg.id });
420
984
  },
421
985
  }),
422
986
 
@@ -431,31 +995,8 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
431
995
  .describe("Include cross-workspace"),
432
996
  },
433
997
  async execute(args) {
434
- try {
435
- const content =
436
- await $`cat .reservations/messages.jsonl 2>/dev/null`.text();
437
- if (!content.trim()) return j({ msgs: [], count: 0 });
438
-
439
- let msgs = content
440
- .trim()
441
- .split("\n")
442
- .map((line) => {
443
- try {
444
- return JSON.parse(line);
445
- } catch {
446
- return null;
447
- }
448
- })
449
- .filter((m) => m !== null)
450
- .filter((m) => m.to === "all" || m.to === state.agentId);
451
-
452
- if (args.unread) msgs = msgs.filter((m) => !m.read);
453
- msgs = msgs.slice(-(args.n || 5)).reverse();
454
-
455
- return j({ msgs, count: msgs.length });
456
- } catch {
457
- return j({ msgs: [], count: 0 });
458
- }
998
+ const msgs = await readMessages(args.n || 5, args.unread || false);
999
+ return json({ msgs, count: msgs.length });
459
1000
  },
460
1001
  }),
461
1002
 
@@ -468,19 +1009,19 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
468
1009
  .describe("Include agent info"),
469
1010
  },
470
1011
  async execute(args) {
471
- const [ready, inProgress, reservations] = await Promise.all([
472
- bd("ready", "--json"),
473
- bd("list", "--status", "in_progress", "--json"),
474
- getAllReservations(),
1012
+ const [ready, inProgress, locks] = await Promise.all([
1013
+ bdReady(),
1014
+ bdList({ status: "in_progress" }),
1015
+ getAllLocks(),
475
1016
  ]);
476
1017
 
477
- const result: any = {
1018
+ const result: Record<string, unknown> = {
478
1019
  ws: directory,
479
1020
  team: state.team,
480
1021
  current_task: state.currentTask,
481
- ready: Array.isArray(ready) ? ready.length : 0,
482
- in_progress: Array.isArray(inProgress) ? inProgress.length : 0,
483
- locks: reservations.length,
1022
+ ready: ready.tasks.length,
1023
+ in_progress: inProgress.tasks.length,
1024
+ locks: locks.length,
484
1025
  };
485
1026
 
486
1027
  if (args.include_agents) {
@@ -491,7 +1032,7 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
491
1032
  };
492
1033
  }
493
1034
 
494
- return j(result);
1035
+ return json(result);
495
1036
  },
496
1037
  }),
497
1038
 
@@ -499,8 +1040,8 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
499
1040
  description: "Sync with git. Pull/push changes.",
500
1041
  args: {},
501
1042
  async execute() {
502
- const result = await bd("sync");
503
- return j({ ok: 1, output: result.output });
1043
+ const result = await bdSync();
1044
+ return json({ ok: 1, output: result.output });
504
1045
  },
505
1046
  }),
506
1047
 
@@ -513,12 +1054,8 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
513
1054
  .describe("Delete closed >N days"),
514
1055
  },
515
1056
  async execute(args) {
516
- const result = await bd(
517
- "cleanup",
518
- "--older-than",
519
- `${args.days || 2}d`,
520
- );
521
- return j({ ok: 1, output: result.output });
1057
+ const result = await bdCleanup(args.days || 2);
1058
+ return json({ ok: 1, output: result.output });
522
1059
  },
523
1060
  }),
524
1061
 
@@ -526,141 +1063,191 @@ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
526
1063
  description: "Check/repair database health.",
527
1064
  args: {},
528
1065
  async execute() {
529
- const result = await bd("doctor", "--fix");
530
- return j({ ok: 1, output: result.output });
1066
+ const result = await bdDoctor();
1067
+ return json({ ok: 1, output: result.output });
531
1068
  },
532
1069
  }),
533
1070
 
534
- bd_insights: tool({
535
- description:
536
- "Graph analysis: bottlenecks, keystones, cycles, PageRank.",
1071
+ bd_dep: tool({
1072
+ description: "Manage dependencies. action: add|remove|tree",
1073
+ args: {
1074
+ action: tool.schema.string().describe("add|remove|tree"),
1075
+ child: tool.schema.string().describe("Child issue ID"),
1076
+ parent: tool.schema
1077
+ .string()
1078
+ .optional()
1079
+ .describe("Parent issue ID (for add/remove)"),
1080
+ type: tool.schema
1081
+ .string()
1082
+ .default("blocks")
1083
+ .describe("Dependency type: blocks|related|parent"),
1084
+ },
1085
+ async execute(args) {
1086
+ if (!args.action || !args.child)
1087
+ return json({ error: "action and child required" });
1088
+
1089
+ if (args.action === "tree") {
1090
+ const result = await bdDepTree(args.child);
1091
+ if (result.error) return json({ error: result.error });
1092
+ return json({ tree: result.output });
1093
+ }
1094
+
1095
+ if (!args.parent)
1096
+ return json({ error: "parent required for add/remove" });
1097
+
1098
+ if (args.action === "add") {
1099
+ const result = await bdDepAdd(args.child, args.parent, args.type);
1100
+ if (result.error) return json({ error: result.error });
1101
+ return json({
1102
+ ok: 1,
1103
+ child: args.child,
1104
+ parent: args.parent,
1105
+ type: args.type,
1106
+ });
1107
+ }
1108
+
1109
+ if (args.action === "remove") {
1110
+ const result = await bdDepRemove(args.child, args.parent);
1111
+ if (result.error) return json({ error: result.error });
1112
+ return json({
1113
+ ok: 1,
1114
+ removed: { child: args.child, parent: args.parent },
1115
+ });
1116
+ }
1117
+
1118
+ return json({ error: "action must be add|remove|tree" });
1119
+ },
1120
+ }),
1121
+
1122
+ bd_blocked: tool({
1123
+ description: "Show blocked issues (have unresolved dependencies).",
537
1124
  args: {},
538
1125
  async execute() {
539
- const result = await bd("stats", "--json");
540
- if (result.error) return j({ error: result.error });
541
-
542
- const tasks = await bd("list", "--json");
543
- const taskList = Array.isArray(tasks) ? tasks : [];
544
-
545
- const blocked = taskList.filter((t: any) => t.status === "blocked");
546
- const highPri = taskList.filter(
547
- (t: any) => t.priority <= 1 && t.status === "open",
548
- );
549
-
550
- return j({
551
- total: taskList.length,
552
- blocked: blocked.length,
553
- high_priority: highPri.length,
554
- bottlenecks: blocked.map((t: any) => ({ id: t.id, t: t.title })),
555
- keystones: highPri.map((t: any) => ({
1126
+ const { tasks, error } = await bdBlocked();
1127
+ if (error) return json({ error });
1128
+
1129
+ return json({
1130
+ items: tasks.map((t) => ({
556
1131
  id: t.id,
557
1132
  t: t.title,
558
1133
  p: t.priority,
559
1134
  })),
1135
+ count: tasks.length,
560
1136
  });
561
1137
  },
562
1138
  }),
563
1139
 
564
- bd_plan: tool({
565
- description: "Parallel execution plan with tracks.",
566
- args: {},
567
- async execute() {
568
- const ready = await bd("ready", "--json");
569
- const tasks = Array.isArray(ready) ? ready : [];
570
-
571
- const tracks: Record<number, any[]> = {};
572
- for (const task of tasks) {
573
- const p = task.priority || 2;
574
- if (!tracks[p]) tracks[p] = [];
575
- tracks[p].push({ id: task.id, t: task.title });
576
- }
1140
+ bd_reopen: tool({
1141
+ description: "Reopen a closed issue.",
1142
+ args: {
1143
+ id: tool.schema.string().describe("Issue ID to reopen"),
1144
+ },
1145
+ async execute(args) {
1146
+ if (!args.id) return json({ error: "id required" });
577
1147
 
578
- return j({
579
- tracks: Object.entries(tracks).map(([pri, items]) => ({
580
- priority: Number(pri),
581
- parallel: items,
582
- })),
583
- total_ready: tasks.length,
584
- });
1148
+ const result = await bdReopen(args.id);
1149
+ if (result.error) return json({ error: result.error });
1150
+
1151
+ return json({ ok: 1, reopened: args.id });
585
1152
  },
586
1153
  }),
587
1154
 
588
- bd_priority: tool({
589
- description: "Priority recommendations based on graph analysis.",
1155
+ bd_update: tool({
1156
+ description:
1157
+ "Update issue properties (title, status, priority, assignee, dependencies).",
590
1158
  args: {
591
- limit: tool.schema
1159
+ id: tool.schema.string().describe("Issue ID to update"),
1160
+ title: tool.schema.string().optional().describe("New title"),
1161
+ status: tool.schema.string().optional().describe("New status"),
1162
+ priority: tool.schema
592
1163
  .number()
593
- .default(5)
594
- .describe("Max issues to return"),
1164
+ .optional()
1165
+ .describe("New priority (0-4)"),
1166
+ assignee: tool.schema
1167
+ .string()
1168
+ .optional()
1169
+ .describe("Assign to user/role"),
1170
+ removeDep: tool.schema
1171
+ .string()
1172
+ .optional()
1173
+ .describe("Remove dependency"),
1174
+ addDep: tool.schema.string().optional().describe("Add dependency"),
595
1175
  },
596
1176
  async execute(args) {
597
- const ready = await bd("ready", "--json");
598
- const tasks = Array.isArray(ready) ? ready : [];
1177
+ if (!args.id) return json({ error: "id required" });
1178
+
1179
+ const result = await bdUpdate(args.id, {
1180
+ title: args.title,
1181
+ status: args.status,
1182
+ priority: args.priority,
1183
+ assignee: args.assignee,
1184
+ removeDep: args.removeDep,
1185
+ addDep: args.addDep,
1186
+ });
1187
+ if (result.error) return json({ error: result.error });
1188
+
1189
+ return json({ ok: 1, updated: args.id });
1190
+ },
1191
+ }),
599
1192
 
600
- const sorted = tasks
601
- .sort((a: any, b: any) => (a.priority || 2) - (b.priority || 2))
602
- .slice(0, args.limit || 5);
1193
+ bd_ready: tool({
1194
+ description: "List ready-to-work tasks (no unresolved dependencies).",
1195
+ args: {
1196
+ sort: tool.schema.string().optional().describe("Sort field"),
1197
+ limit: tool.schema.number().optional().describe("Max results"),
1198
+ assignee: tool.schema
1199
+ .string()
1200
+ .optional()
1201
+ .describe("Filter by assignee"),
1202
+ label: tool.schema.string().optional().describe("Filter by label"),
1203
+ },
1204
+ async execute(args) {
1205
+ const result = await bdReady({
1206
+ sort: args.sort,
1207
+ limit: args.limit,
1208
+ assignee: args.assignee,
1209
+ label: args.label,
1210
+ });
1211
+ if (result.error) return json({ error: result.error });
603
1212
 
604
- return j({
605
- recommended: sorted.map((t: any) => ({
1213
+ return json({
1214
+ items: result.tasks.map((t: Task) => ({
606
1215
  id: t.id,
607
- t: t.title,
608
- p: t.priority,
609
- reason:
610
- t.priority === 0
611
- ? "critical"
612
- : t.priority === 1
613
- ? "high priority"
614
- : "ready",
1216
+ title: t.title,
1217
+ priority: t.priority,
615
1218
  })),
1219
+ count: result.tasks.length,
616
1220
  });
617
1221
  },
618
1222
  }),
619
1223
 
620
- bd_diff: tool({
621
- description: "Compare issue changes between git revisions.",
1224
+ bd_search: tool({
1225
+ description: "Search issues by text query.",
622
1226
  args: {
623
- since: tool.schema
624
- .string()
625
- .optional()
626
- .describe("Start revision (commit, tag, date)"),
627
- as_of: tool.schema
628
- .string()
629
- .optional()
630
- .describe("End revision (default: current)"),
1227
+ query: tool.schema.string().describe("Search text"),
631
1228
  },
632
1229
  async execute(args) {
633
- const sinceArg = args.since || "HEAD~10";
634
- const asOfArg = args.as_of || "HEAD";
1230
+ if (!args.query) return json({ error: "query required" });
635
1231
 
636
- try {
637
- const diff =
638
- await $`git diff ${sinceArg}..${asOfArg} -- .beads/`.text();
639
- const lines = diff.split("\n");
640
-
641
- const added = lines.filter(
642
- (l) => l.startsWith("+") && !l.startsWith("+++"),
643
- ).length;
644
- const removed = lines.filter(
645
- (l) => l.startsWith("-") && !l.startsWith("---"),
646
- ).length;
647
-
648
- return j({
649
- since: sinceArg,
650
- as_of: asOfArg,
651
- changes: { added, removed },
652
- summary: diff.length > 500 ? diff.slice(0, 500) + "..." : diff,
653
- });
654
- } catch (e: any) {
655
- return j({ error: e.message });
656
- }
1232
+ const { tasks, error } = await bdSearch(args.query);
1233
+ if (error) return json({ error });
1234
+
1235
+ return json({
1236
+ items: tasks.map((t) => ({
1237
+ id: t.id,
1238
+ t: t.title,
1239
+ p: t.priority,
1240
+ s: t.status,
1241
+ })),
1242
+ count: tasks.length,
1243
+ });
657
1244
  },
658
1245
  }),
659
1246
  },
660
1247
 
661
1248
  event: async ({ event }) => {
662
1249
  if (event.type === "session.idle" && state.currentTask) {
663
- await $`bd sync`.quiet().catch(() => {});
1250
+ await bdSync();
664
1251
  }
665
1252
  },
666
1253
  };