opencodekit 0.12.4 → 0.12.5

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.
Files changed (60) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/template/.opencode/command/accessibility-check.md +7 -10
  3. package/dist/template/.opencode/command/analyze-mockup.md +3 -16
  4. package/dist/template/.opencode/command/analyze-project.md +57 -69
  5. package/dist/template/.opencode/command/brainstorm.md +3 -11
  6. package/dist/template/.opencode/command/commit.md +10 -18
  7. package/dist/template/.opencode/command/create.md +4 -8
  8. package/dist/template/.opencode/command/design-audit.md +24 -51
  9. package/dist/template/.opencode/command/design.md +10 -17
  10. package/dist/template/.opencode/command/finish.md +9 -9
  11. package/dist/template/.opencode/command/fix-ci.md +7 -28
  12. package/dist/template/.opencode/command/fix-types.md +3 -7
  13. package/dist/template/.opencode/command/fix-ui.md +5 -11
  14. package/dist/template/.opencode/command/fix.md +4 -10
  15. package/dist/template/.opencode/command/handoff.md +8 -14
  16. package/dist/template/.opencode/command/implement.md +13 -16
  17. package/dist/template/.opencode/command/import-plan.md +20 -38
  18. package/dist/template/.opencode/command/init.md +9 -13
  19. package/dist/template/.opencode/command/integration-test.md +11 -13
  20. package/dist/template/.opencode/command/issue.md +4 -8
  21. package/dist/template/.opencode/command/new-feature.md +20 -40
  22. package/dist/template/.opencode/command/plan.md +8 -12
  23. package/dist/template/.opencode/command/pr.md +29 -38
  24. package/dist/template/.opencode/command/quick-build.md +3 -7
  25. package/dist/template/.opencode/command/research-and-implement.md +4 -6
  26. package/dist/template/.opencode/command/research.md +10 -7
  27. package/dist/template/.opencode/command/resume.md +12 -24
  28. package/dist/template/.opencode/command/revert-feature.md +21 -56
  29. package/dist/template/.opencode/command/review-codebase.md +21 -23
  30. package/dist/template/.opencode/command/skill-create.md +1 -5
  31. package/dist/template/.opencode/command/skill-optimize.md +3 -10
  32. package/dist/template/.opencode/command/status.md +28 -25
  33. package/dist/template/.opencode/command/triage.md +19 -31
  34. package/dist/template/.opencode/command/ui-review.md +6 -13
  35. package/dist/template/.opencode/command.backup/analyze-project.md +465 -0
  36. package/dist/template/.opencode/command.backup/finish.md +167 -0
  37. package/dist/template/.opencode/command.backup/implement.md +143 -0
  38. package/dist/template/.opencode/command.backup/pr.md +252 -0
  39. package/dist/template/.opencode/command.backup/status.md +376 -0
  40. package/dist/template/.opencode/memory/project/SHELL_OUTPUT_MIGRATION_PLAN.md +551 -0
  41. package/dist/template/.opencode/memory/project/gotchas.md +33 -28
  42. package/dist/template/.opencode/opencode.json +14 -28
  43. package/dist/template/.opencode/package.json +1 -3
  44. package/dist/template/.opencode/plugin/compaction.ts +51 -129
  45. package/dist/template/.opencode/plugin/handoff.ts +18 -163
  46. package/dist/template/.opencode/plugin/notification.ts +1 -1
  47. package/dist/template/.opencode/plugin/package.json +7 -0
  48. package/dist/template/.opencode/plugin/sessions.ts +185 -651
  49. package/dist/template/.opencode/plugin/skill-mcp.ts +2 -1
  50. package/dist/template/.opencode/plugin/truncator.ts +19 -41
  51. package/dist/template/.opencode/plugin/tsconfig.json +14 -13
  52. package/dist/template/.opencode/tool/bd-inbox.ts +109 -0
  53. package/dist/template/.opencode/tool/bd-msg.ts +62 -0
  54. package/dist/template/.opencode/tool/bd-release.ts +71 -0
  55. package/dist/template/.opencode/tool/bd-reserve.ts +120 -0
  56. package/package.json +2 -2
  57. package/dist/template/.opencode/plugin/beads.ts +0 -1419
  58. package/dist/template/.opencode/plugin/compactor.ts +0 -107
  59. package/dist/template/.opencode/plugin/enforcer.ts +0 -190
  60. package/dist/template/.opencode/plugin/injector.ts +0 -150
@@ -1,1419 +0,0 @@
1
- import { createHash } from "crypto";
2
- import { type Plugin, tool } from "@opencode-ai/plugin";
3
-
4
- // =============================================================================
5
- // Types
6
- // =============================================================================
7
-
8
- interface TaskState {
9
- currentTask: string | null;
10
- reservedFiles: Set<string>;
11
- team: string;
12
- role: string;
13
- initialized: boolean;
14
- agentId: string;
15
- }
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
-
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
-
77
- const state: TaskState = {
78
- currentTask: null,
79
- reservedFiles: new Set(),
80
- team: "default",
81
- role: "",
82
- initialized: false,
83
- agentId,
84
- };
85
-
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 }> {
307
- try {
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
- overrideAgent?: string,
370
- ): Promise<{ acquired: boolean; holder?: string }> {
371
- const lockPath = lockDir(filePath);
372
- const now = Date.now();
373
- const expires = now + ttlSeconds * 1000;
374
- const agentToUse = overrideAgent || state.agentId;
375
-
376
- // Atomic: mkdir fails if dir exists
377
- try {
378
- await $`mkdir ${lockPath}`.cwd(directory);
379
- } catch {
380
- // Lock exists - check if expired
381
- try {
382
- const metaPath = `${lockPath}/meta.json`;
383
- const content = await $`cat ${metaPath}`.cwd(directory).text();
384
- const lock: LockData = JSON.parse(content);
385
-
386
- if (lock.expires < now) {
387
- // Expired - remove and retry
388
- await $`rm -rf ${lockPath}`.cwd(directory).quiet();
389
- return acquireLock(filePath, reason, ttlSeconds, overrideAgent);
390
- }
391
-
392
- if (lock.agent === agentToUse) {
393
- // We already hold this lock - refresh it
394
- lock.expires = expires;
395
- await $`echo ${JSON.stringify(lock)} > ${metaPath}`
396
- .cwd(directory)
397
- .quiet();
398
- return { acquired: true };
399
- }
400
-
401
- return { acquired: false, holder: lock.agent };
402
- } catch {
403
- // Corrupted lock - remove and retry
404
- await $`rm -rf ${lockPath}`.cwd(directory).quiet();
405
- return acquireLock(filePath, reason, ttlSeconds, overrideAgent);
406
- }
407
- }
408
-
409
- // Lock acquired - write metadata
410
- const lockData: LockData = {
411
- path: filePath,
412
- agent: agentToUse,
413
- reason,
414
- created: now,
415
- expires,
416
- task: state.currentTask,
417
- };
418
-
419
- const metaPath = `${lockPath}/meta.json`;
420
- await $`echo ${JSON.stringify(lockData)} > ${metaPath}`
421
- .cwd(directory)
422
- .quiet();
423
- state.reservedFiles.add(filePath);
424
- return { acquired: true };
425
- }
426
-
427
- async function releaseLock(filePath: string): Promise<void> {
428
- const lockPath = lockDir(filePath);
429
- await $`rm -rf ${lockPath}`
430
- .cwd(directory)
431
- .quiet()
432
- .catch(() => {});
433
- state.reservedFiles.delete(filePath);
434
- }
435
-
436
- async function getAllLocks(): Promise<LockData[]> {
437
- try {
438
- const result =
439
- await $`find ${RESERVATIONS_DIR} -name "meta.json" -type f 2>/dev/null`
440
- .cwd(directory)
441
- .text()
442
- .catch(() => "");
443
-
444
- if (!result.trim()) return [];
445
-
446
- const locks: LockData[] = [];
447
- const now = Date.now();
448
-
449
- for (const metaPath of result.trim().split("\n")) {
450
- try {
451
- const content = await $`cat ${metaPath}`.cwd(directory).text();
452
- const lock: LockData = JSON.parse(content);
453
- if (lock.expires > now) {
454
- locks.push(lock);
455
- }
456
- } catch {
457
- // Skip invalid
458
- }
459
- }
460
- return locks;
461
- } catch {
462
- return [];
463
- }
464
- }
465
-
466
- async function cleanupExpiredLocks(): Promise<number> {
467
- let cleaned = 0;
468
- try {
469
- const result =
470
- await $`find ${RESERVATIONS_DIR} -name "meta.json" -type f 2>/dev/null`
471
- .cwd(directory)
472
- .text()
473
- .catch(() => "");
474
-
475
- if (!result.trim()) return 0;
476
-
477
- const now = Date.now();
478
- for (const metaPath of result.trim().split("\n")) {
479
- try {
480
- const content = await $`cat ${metaPath}`.cwd(directory).text();
481
- const lock: LockData = JSON.parse(content);
482
- if (lock.expires < now) {
483
- const lockDir = metaPath.replace("/meta.json", "");
484
- await $`rm -rf ${lockDir}`.cwd(directory).quiet();
485
- cleaned++;
486
- }
487
- } catch {
488
- // Remove corrupted lock
489
- const lockDir = metaPath.replace("/meta.json", "");
490
- await $`rm -rf ${lockDir}`.cwd(directory).quiet();
491
- cleaned++;
492
- }
493
- }
494
- } catch {
495
- // Ignore
496
- }
497
- return cleaned;
498
- }
499
-
500
- // =============================================================================
501
- // Message Helpers
502
- // =============================================================================
503
-
504
- async function ensureReservationsDir(): Promise<void> {
505
- await $`mkdir -p ${RESERVATIONS_DIR}`.cwd(directory).quiet();
506
- }
507
-
508
- async function appendMessage(msg: Message): Promise<void> {
509
- await ensureReservationsDir();
510
- await $`echo ${JSON.stringify(msg)} >> ${RESERVATIONS_DIR}/messages.jsonl`
511
- .cwd(directory)
512
- .quiet();
513
- }
514
-
515
- async function readMessages(
516
- limit: number,
517
- unreadOnly: boolean,
518
- ): Promise<Message[]> {
519
- try {
520
- const content =
521
- await $`cat ${RESERVATIONS_DIR}/messages.jsonl 2>/dev/null`
522
- .cwd(directory)
523
- .text();
524
-
525
- if (!content.trim()) return [];
526
-
527
- let msgs: Message[] = content
528
- .trim()
529
- .split("\n")
530
- .map((line) => {
531
- try {
532
- return JSON.parse(line) as Message;
533
- } catch {
534
- return null;
535
- }
536
- })
537
- .filter((m): m is Message => m !== null)
538
- .filter((m) => m.to === "all" || m.to === state.agentId);
539
-
540
- if (unreadOnly) msgs = msgs.filter((m) => !m.read);
541
- return msgs.slice(-limit).reverse();
542
- } catch {
543
- return [];
544
- }
545
- }
546
-
547
- async function cleanupOldMessages(maxAgeDays: number): Promise<number> {
548
- try {
549
- const content =
550
- await $`cat ${RESERVATIONS_DIR}/messages.jsonl 2>/dev/null`
551
- .cwd(directory)
552
- .text();
553
-
554
- if (!content.trim()) return 0;
555
-
556
- const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
557
- const msgs = content
558
- .trim()
559
- .split("\n")
560
- .map((line) => {
561
- try {
562
- return JSON.parse(line) as Message;
563
- } catch {
564
- return null;
565
- }
566
- })
567
- .filter((m): m is Message => m !== null && m.at > cutoff);
568
-
569
- const originalCount = content.trim().split("\n").length;
570
- const newContent = msgs.map((m) => JSON.stringify(m)).join("\n");
571
- await $`echo ${newContent} > ${RESERVATIONS_DIR}/messages.jsonl`
572
- .cwd(directory)
573
- .quiet();
574
-
575
- return originalCount - msgs.length;
576
- } catch {
577
- return 0;
578
- }
579
- }
580
-
581
- // =============================================================================
582
- // JSON Response Helper
583
- // =============================================================================
584
-
585
- function json(data: Record<string, unknown>): string {
586
- return JSON.stringify(data, null, 0);
587
- }
588
-
589
- // =============================================================================
590
- // Tools
591
- // =============================================================================
592
-
593
- return {
594
- tool: {
595
- bd_init: tool({
596
- description: "Join beads workspace. MUST call first.",
597
- args: {
598
- team: tool.schema.string().optional().describe("Team name"),
599
- role: tool.schema
600
- .string()
601
- .optional()
602
- .describe("Role: build|rush|explore|planner|review|scout|vision"),
603
- },
604
- async execute(args) {
605
- await $`bd init`
606
- .cwd(directory)
607
- .quiet()
608
- .catch(() => {});
609
- await ensureReservationsDir();
610
-
611
- state.team = args.team || "default";
612
- state.role = args.role || "";
613
- state.initialized = true;
614
- state.currentTask = null;
615
- state.reservedFiles.clear();
616
-
617
- // Cleanup expired locks on init
618
- const cleaned = await cleanupExpiredLocks();
619
-
620
- return json({
621
- ok: 1,
622
- agent: state.agentId,
623
- ws: directory,
624
- team: state.team,
625
- role: state.role || undefined,
626
- cleaned_locks: cleaned || undefined,
627
- });
628
- },
629
- }),
630
-
631
- bd_claim: tool({
632
- description: "Claim next ready task. Auto-syncs, marks in_progress.",
633
- args: {},
634
- async execute() {
635
- if (!state.initialized)
636
- return json({ error: "Call bd_init() first" });
637
-
638
- await bdSync();
639
- const { tasks, error } = await bdReady();
640
- if (error) return json({ error });
641
- if (!tasks.length) return json({ msg: "no ready tasks" });
642
-
643
- let task = tasks[0];
644
- if (state.role) {
645
- const roleTask = tasks.find((t) => t.tags?.includes(state.role));
646
- if (roleTask) task = roleTask;
647
- }
648
-
649
- const updateResult = await bdUpdate(task.id, {
650
- status: "in_progress",
651
- });
652
- if (updateResult.error) return json({ error: updateResult.error });
653
-
654
- state.currentTask = task.id;
655
-
656
- return json({
657
- id: task.id,
658
- t: task.title,
659
- p: task.priority,
660
- type: task.type,
661
- });
662
- },
663
- }),
664
-
665
- bd_done: tool({
666
- description:
667
- "Complete task. Auto-releases files, syncs. Restart session after.",
668
- args: {
669
- id: tool.schema.string().describe("Task ID"),
670
- msg: tool.schema
671
- .string()
672
- .default("completed")
673
- .describe("Completion message"),
674
- },
675
- async execute(args, context) {
676
- const taskId = args.id || state.currentTask;
677
- if (!taskId) return json({ error: "No task ID" });
678
-
679
- // Audit trail
680
- const auditAgent = context?.agent || state.agentId;
681
-
682
- const closeResult = await bdClose(
683
- taskId,
684
- `${args.msg} [by ${auditAgent}]`,
685
- );
686
- if (closeResult.error) return json({ error: closeResult.error });
687
-
688
- // Release all locks
689
- for (const path of state.reservedFiles) {
690
- await releaseLock(path);
691
- }
692
-
693
- await bdSync();
694
- state.currentTask = null;
695
-
696
- return json({ ok: 1, closed: taskId, hint: "Restart session" });
697
- },
698
- }),
699
-
700
- bd_add: tool({
701
- description: "Create issue. Use tags to assign to roles.",
702
- args: {
703
- title: tool.schema.string().describe("Actionable title"),
704
- desc: tool.schema
705
- .string()
706
- .optional()
707
- .describe("Why/what/how context"),
708
- pri: tool.schema
709
- .number()
710
- .default(2)
711
- .describe("0=critical,1=high,2=normal,3=low,4=backlog"),
712
- type: tool.schema
713
- .string()
714
- .default("task")
715
- .describe("task|bug|feature|epic|chore"),
716
- tags: tool.schema
717
- .array(tool.schema.string())
718
- .optional()
719
- .describe(
720
- "Agent tags: build,rush,explore,planner,review,scout,vision",
721
- ),
722
- parent: tool.schema.string().optional().describe("Parent issue ID"),
723
- deps: tool.schema
724
- .array(tool.schema.string())
725
- .optional()
726
- .describe("Dependencies (type:id format)"),
727
- assignee: tool.schema.string().optional().describe("Assignee name"),
728
- estimate: tool.schema
729
- .number()
730
- .optional()
731
- .describe("Time estimate in minutes (e.g. 60 for 1h)"),
732
- acceptance: tool.schema
733
- .string()
734
- .optional()
735
- .describe("Acceptance criteria"),
736
- },
737
- async execute(args) {
738
- if (!args.title) return json({ error: "title required" });
739
-
740
- const result = await bdCreate(args.title, {
741
- priority: args.pri,
742
- type: args.type,
743
- description: args.desc,
744
- parent: args.parent,
745
- tags: args.tags,
746
- deps: args.deps,
747
- assignee: args.assignee,
748
- estimate: args.estimate,
749
- acceptance: args.acceptance,
750
- });
751
-
752
- if (result.error) return json({ error: result.error });
753
-
754
- return json({
755
- id: result.id,
756
- t: args.title,
757
- p: args.pri || 2,
758
- });
759
- },
760
- }),
761
-
762
- bd_assign: tool({
763
- description:
764
- "Assign task to role (leader only). Adds tag and notifies.",
765
- args: {
766
- id: tool.schema.string().describe("Issue ID"),
767
- role: tool.schema
768
- .string()
769
- .describe("Role: build|rush|explore|planner|review|scout|vision"),
770
- notify: tool.schema
771
- .boolean()
772
- .default(true)
773
- .describe("Broadcast notification"),
774
- },
775
- async execute(args) {
776
- if (!args.id || !args.role)
777
- return json({ error: "id and role required" });
778
-
779
- const result = await bdUpdate(args.id, { addLabel: args.role });
780
- if (result.error) return json({ error: result.error });
781
-
782
- if (args.notify !== false) {
783
- await appendMessage({
784
- id: `notify-${Date.now().toString(36)}`,
785
- from: state.agentId,
786
- to: "all",
787
- subj: `Task ${args.id} assigned to ${args.role}`,
788
- importance: "normal",
789
- at: Date.now(),
790
- read: false,
791
- });
792
- }
793
-
794
- return json({ ok: 1, id: args.id, role: args.role });
795
- },
796
- }),
797
-
798
- bd_ls: tool({
799
- description: "List issues. status: open|closed|in_progress|ready|all",
800
- args: {
801
- status: tool.schema.string().default("open"),
802
- limit: tool.schema.number().default(10),
803
- offset: tool.schema.number().default(0),
804
- sort: tool.schema
805
- .string()
806
- .optional()
807
- .describe("Sort by: priority|created|updated|title"),
808
- reverse: tool.schema
809
- .boolean()
810
- .optional()
811
- .describe("Reverse sort order"),
812
- label: tool.schema.string().optional().describe("Filter by label"),
813
- assignee: tool.schema
814
- .string()
815
- .optional()
816
- .describe("Filter by assignee"),
817
- type: tool.schema
818
- .string()
819
- .optional()
820
- .describe("Filter by type: task|bug|feature|epic"),
821
- priorityMin: tool.schema
822
- .number()
823
- .optional()
824
- .describe("Min priority (0-4)"),
825
- priorityMax: tool.schema
826
- .number()
827
- .optional()
828
- .describe("Max priority (0-4)"),
829
- },
830
- async execute(args) {
831
- const status = args.status || "open";
832
- const limit = Math.min(args.limit || 10, 50);
833
- const offset = args.offset || 0;
834
-
835
- const opts = {
836
- status: status === "all" ? undefined : status,
837
- sort: args.sort,
838
- reverse: args.reverse,
839
- label: args.label,
840
- assignee: args.assignee,
841
- type: args.type,
842
- priorityMin: args.priorityMin,
843
- priorityMax: args.priorityMax,
844
- };
845
-
846
- const result =
847
- status === "ready"
848
- ? await bdReady({
849
- sort: args.sort,
850
- limit,
851
- assignee: args.assignee,
852
- label: args.label,
853
- })
854
- : await bdList(opts);
855
-
856
- if (result.error) return json({ error: result.error });
857
-
858
- const items = result.tasks.slice(offset, offset + limit).map((t) => ({
859
- id: t.id,
860
- t: t.title,
861
- p: t.priority,
862
- s: t.status,
863
- tags: t.tags?.length ? t.tags : undefined,
864
- }));
865
-
866
- return json({
867
- items,
868
- count: items.length,
869
- total: result.tasks.length,
870
- });
871
- },
872
- }),
873
-
874
- bd_show: tool({
875
- description: "Get full issue details.",
876
- args: { id: tool.schema.string().describe("Issue ID") },
877
- async execute(args) {
878
- if (!args.id) return json({ error: "id required" });
879
- const result = await bdShow(args.id);
880
- if (result.error) return json({ error: result.error });
881
- return json(result.task as unknown as Record<string, unknown>);
882
- },
883
- }),
884
-
885
- bd_reserve: tool({
886
- description: "Lock files for editing. Prevents conflicts.",
887
- args: {
888
- paths: tool.schema
889
- .array(tool.schema.string())
890
- .describe("Files to lock"),
891
- reason: tool.schema.string().optional().describe("Why reserving"),
892
- ttl: tool.schema
893
- .number()
894
- .default(600)
895
- .describe("Seconds until expiry"),
896
- },
897
- async execute(args, context) {
898
- if (!args.paths?.length) return json({ error: "paths required" });
899
-
900
- // Log who is reserving for audit
901
- const reservingAgent = context?.agent || state.agentId;
902
-
903
- await ensureReservationsDir();
904
-
905
- const granted: string[] = [];
906
- const conflicts: { path: string; holder?: string }[] = [];
907
-
908
- for (const path of args.paths) {
909
- const result = await acquireLock(
910
- path,
911
- args.reason,
912
- args.ttl,
913
- reservingAgent,
914
- );
915
- if (result.acquired) {
916
- granted.push(path);
917
- } else {
918
- conflicts.push({ path, holder: result.holder });
919
- }
920
- }
921
-
922
- const response: Record<string, unknown> = { granted };
923
- if (conflicts.length) response.conflicts = conflicts;
924
- return json(response);
925
- },
926
- }),
927
-
928
- bd_release: tool({
929
- description: "Unlock files. Auto-released on done().",
930
- args: {
931
- paths: tool.schema
932
- .array(tool.schema.string())
933
- .optional()
934
- .describe("Files to unlock (empty=all)"),
935
- },
936
- async execute(args) {
937
- const toRelease = args.paths?.length
938
- ? args.paths
939
- : [...state.reservedFiles];
940
-
941
- for (const path of toRelease) {
942
- await releaseLock(path);
943
- }
944
-
945
- return json({ released: toRelease });
946
- },
947
- }),
948
-
949
- bd_reservations: tool({
950
- description: "List active file locks. Check before editing.",
951
- args: {},
952
- async execute() {
953
- const locks = await getAllLocks();
954
- return json({
955
- locks: locks.map((r) => ({
956
- path: r.path,
957
- agent: r.agent,
958
- expires: new Date(r.expires).toISOString(),
959
- task: r.task,
960
- })),
961
- count: locks.length,
962
- });
963
- },
964
- }),
965
-
966
- bd_msg: tool({
967
- description:
968
- "Send message. Use global=true + to='all' for team-wide broadcast.",
969
- args: {
970
- subj: tool.schema.string().describe("Subject"),
971
- body: tool.schema.string().optional().describe("Message body"),
972
- to: tool.schema
973
- .string()
974
- .default("all")
975
- .describe("Recipient or 'all'"),
976
- importance: tool.schema
977
- .string()
978
- .default("normal")
979
- .describe("low|normal|high"),
980
- thread: tool.schema.string().optional().describe("Thread ID"),
981
- global: tool.schema
982
- .boolean()
983
- .default(false)
984
- .describe("Send to all workspaces"),
985
- },
986
- async execute(args, context) {
987
- if (!args.subj) return json({ error: "subj required" });
988
-
989
- // Use context agent if available for audit trail
990
- const senderAgent = context?.agent || state.agentId;
991
-
992
- const msg: Message = {
993
- id: `msg-${Date.now().toString(36)}`,
994
- from: senderAgent,
995
- to: args.to || "all",
996
- subj: args.subj,
997
- body: args.body,
998
- importance: args.importance || "normal",
999
- thread: args.thread,
1000
- global: args.global,
1001
- at: Date.now(),
1002
- read: false,
1003
- };
1004
-
1005
- await appendMessage(msg);
1006
- return json({ ok: 1, id: msg.id });
1007
- },
1008
- }),
1009
-
1010
- bd_inbox: tool({
1011
- description: "Get messages. Includes global by default.",
1012
- args: {
1013
- n: tool.schema.number().default(5).describe("Max messages"),
1014
- unread: tool.schema.boolean().default(false).describe("Unread only"),
1015
- global: tool.schema
1016
- .boolean()
1017
- .default(true)
1018
- .describe("Include cross-workspace"),
1019
- },
1020
- async execute(args) {
1021
- const msgs = await readMessages(args.n || 5, args.unread || false);
1022
- return json({ msgs, count: msgs.length });
1023
- },
1024
- }),
1025
-
1026
- bd_ack: tool({
1027
- description: "Acknowledge message(s). Marks as read.",
1028
- args: {
1029
- ids: tool.schema
1030
- .array(tool.schema.string())
1031
- .describe("Message IDs to acknowledge"),
1032
- },
1033
- async execute(args) {
1034
- if (!args.ids?.length) return json({ error: "ids required" });
1035
-
1036
- try {
1037
- const content =
1038
- await $`cat ${RESERVATIONS_DIR}/messages.jsonl 2>/dev/null`
1039
- .cwd(directory)
1040
- .text();
1041
-
1042
- if (!content.trim()) return json({ acked: 0 });
1043
-
1044
- const idsToAck = new Set(args.ids);
1045
- let acked = 0;
1046
-
1047
- const msgs = content
1048
- .trim()
1049
- .split("\n")
1050
- .map((line) => {
1051
- try {
1052
- const msg = JSON.parse(line) as Message;
1053
- if (idsToAck.has(msg.id) && !msg.read) {
1054
- msg.read = true;
1055
- acked++;
1056
- }
1057
- return msg;
1058
- } catch {
1059
- return null;
1060
- }
1061
- })
1062
- .filter((m): m is Message => m !== null);
1063
-
1064
- const newContent = msgs.map((m) => JSON.stringify(m)).join("\n");
1065
- await $`echo ${newContent} > ${RESERVATIONS_DIR}/messages.jsonl`
1066
- .cwd(directory)
1067
- .quiet();
1068
-
1069
- return json({ ok: 1, acked });
1070
- } catch {
1071
- return json({ acked: 0 });
1072
- }
1073
- },
1074
- }),
1075
-
1076
- bd_whois: tool({
1077
- description: "Agent directory lookup. See who's working on what.",
1078
- args: {
1079
- agent: tool.schema
1080
- .string()
1081
- .optional()
1082
- .describe("Agent ID to lookup (empty=all)"),
1083
- },
1084
- async execute(args) {
1085
- const locks = await getAllLocks();
1086
- const { tasks: inProgressTasks } = await bdList({
1087
- status: "in_progress",
1088
- });
1089
-
1090
- // Build agent activity map
1091
- const agentMap: Record<
1092
- string,
1093
- {
1094
- files: string[];
1095
- task?: string;
1096
- role?: string;
1097
- }
1098
- > = {};
1099
-
1100
- // Add locks info
1101
- for (const lock of locks) {
1102
- if (!agentMap[lock.agent]) {
1103
- agentMap[lock.agent] = { files: [] };
1104
- }
1105
- agentMap[lock.agent].files.push(lock.path);
1106
- if (lock.task) {
1107
- agentMap[lock.agent].task = lock.task;
1108
- }
1109
- }
1110
-
1111
- // Add current agent info
1112
- if (!agentMap[state.agentId]) {
1113
- agentMap[state.agentId] = { files: [] };
1114
- }
1115
- agentMap[state.agentId].role = state.role || undefined;
1116
- if (state.currentTask) {
1117
- agentMap[state.agentId].task = state.currentTask;
1118
- }
1119
-
1120
- // Filter if specific agent requested
1121
- if (args.agent) {
1122
- const agent = agentMap[args.agent];
1123
- if (!agent) return json({ error: "agent not found" });
1124
- return json({ agent: args.agent, ...agent });
1125
- }
1126
-
1127
- return json({
1128
- agents: Object.entries(agentMap).map(([id, info]) => ({
1129
- id,
1130
- ...info,
1131
- })),
1132
- in_progress_tasks: inProgressTasks.length,
1133
- });
1134
- },
1135
- }),
1136
-
1137
- bd_status: tool({
1138
- description: "Workspace overview. Shows agents, tasks, locks.",
1139
- args: {
1140
- include_agents: tool.schema
1141
- .boolean()
1142
- .default(false)
1143
- .describe("Include agent info"),
1144
- },
1145
- async execute(args) {
1146
- const [ready, inProgress, locks] = await Promise.all([
1147
- bdReady(),
1148
- bdList({ status: "in_progress" }),
1149
- getAllLocks(),
1150
- ]);
1151
-
1152
- const result: Record<string, unknown> = {
1153
- ws: directory,
1154
- team: state.team,
1155
- current_task: state.currentTask,
1156
- ready: ready.tasks.length,
1157
- in_progress: inProgress.tasks.length,
1158
- locks: locks.length,
1159
- };
1160
-
1161
- if (args.include_agents) {
1162
- result.agent = {
1163
- id: state.agentId,
1164
- role: state.role || undefined,
1165
- reserved: [...state.reservedFiles],
1166
- };
1167
- }
1168
-
1169
- return json(result);
1170
- },
1171
- }),
1172
-
1173
- bd_sync: tool({
1174
- description: "Sync with git. Pull/push changes.",
1175
- args: {
1176
- reason: tool.schema
1177
- .string()
1178
- .optional()
1179
- .describe("Audit trail reason for sync"),
1180
- },
1181
- async execute(args, context) {
1182
- const result = await bdSync();
1183
- // Log sync with context for audit trail
1184
- const syncAgent = context?.agent || state.agentId;
1185
- return json({
1186
- ok: 1,
1187
- output: result.output,
1188
- by: syncAgent,
1189
- reason: args.reason,
1190
- });
1191
- },
1192
- }),
1193
-
1194
- bd_cleanup: tool({
1195
- description: "Remove old closed issues. Run every few days.",
1196
- args: {
1197
- days: tool.schema
1198
- .number()
1199
- .default(2)
1200
- .describe("Delete closed >N days"),
1201
- },
1202
- async execute(args) {
1203
- const result = await bdCleanup(args.days || 2);
1204
- return json({ ok: 1, output: result.output });
1205
- },
1206
- }),
1207
-
1208
- bd_doctor: tool({
1209
- description: "Check/repair database health.",
1210
- args: {},
1211
- async execute() {
1212
- const result = await bdDoctor();
1213
- return json({ ok: 1, output: result.output });
1214
- },
1215
- }),
1216
-
1217
- bd_dep: tool({
1218
- description: "Manage dependencies. action: add|remove|tree",
1219
- args: {
1220
- action: tool.schema.string().describe("add|remove|tree"),
1221
- child: tool.schema.string().describe("Child issue ID"),
1222
- parent: tool.schema
1223
- .string()
1224
- .optional()
1225
- .describe("Parent issue ID (for add/remove)"),
1226
- type: tool.schema
1227
- .string()
1228
- .default("blocks")
1229
- .describe("Dependency type: blocks|related|parent"),
1230
- },
1231
- async execute(args) {
1232
- if (!args.action || !args.child)
1233
- return json({ error: "action and child required" });
1234
-
1235
- if (args.action === "tree") {
1236
- const result = await bdDepTree(args.child);
1237
- if (result.error) return json({ error: result.error });
1238
- return json({ tree: result.output });
1239
- }
1240
-
1241
- if (!args.parent)
1242
- return json({ error: "parent required for add/remove" });
1243
-
1244
- if (args.action === "add") {
1245
- const result = await bdDepAdd(args.child, args.parent, args.type);
1246
- if (result.error) return json({ error: result.error });
1247
- return json({
1248
- ok: 1,
1249
- child: args.child,
1250
- parent: args.parent,
1251
- type: args.type,
1252
- });
1253
- }
1254
-
1255
- if (args.action === "remove") {
1256
- const result = await bdDepRemove(args.child, args.parent);
1257
- if (result.error) return json({ error: result.error });
1258
- return json({
1259
- ok: 1,
1260
- removed: { child: args.child, parent: args.parent },
1261
- });
1262
- }
1263
-
1264
- return json({ error: "action must be add|remove|tree" });
1265
- },
1266
- }),
1267
-
1268
- bd_blocked: tool({
1269
- description: "Show blocked issues (have unresolved dependencies).",
1270
- args: {},
1271
- async execute() {
1272
- const { tasks, error } = await bdBlocked();
1273
- if (error) return json({ error });
1274
-
1275
- return json({
1276
- items: tasks.map((t) => ({
1277
- id: t.id,
1278
- t: t.title,
1279
- p: t.priority,
1280
- })),
1281
- count: tasks.length,
1282
- });
1283
- },
1284
- }),
1285
-
1286
- bd_reopen: tool({
1287
- description: "Reopen a closed issue.",
1288
- args: {
1289
- id: tool.schema.string().describe("Issue ID to reopen"),
1290
- },
1291
- async execute(args) {
1292
- if (!args.id) return json({ error: "id required" });
1293
-
1294
- const result = await bdReopen(args.id);
1295
- if (result.error) return json({ error: result.error });
1296
-
1297
- return json({ ok: 1, reopened: args.id });
1298
- },
1299
- }),
1300
-
1301
- bd_update: tool({
1302
- description:
1303
- "Update issue properties (title, status, priority, assignee, dependencies).",
1304
- args: {
1305
- id: tool.schema.string().describe("Issue ID to update"),
1306
- title: tool.schema.string().optional().describe("New title"),
1307
- status: tool.schema.string().optional().describe("New status"),
1308
- priority: tool.schema
1309
- .number()
1310
- .optional()
1311
- .describe("New priority (0-4)"),
1312
- assignee: tool.schema
1313
- .string()
1314
- .optional()
1315
- .describe("Assign to user/role"),
1316
- removeDep: tool.schema
1317
- .string()
1318
- .optional()
1319
- .describe("Remove dependency"),
1320
- addDep: tool.schema.string().optional().describe("Add dependency"),
1321
- },
1322
- async execute(args) {
1323
- if (!args.id) return json({ error: "id required" });
1324
-
1325
- const result = await bdUpdate(args.id, {
1326
- title: args.title,
1327
- status: args.status,
1328
- priority: args.priority,
1329
- assignee: args.assignee,
1330
- removeDep: args.removeDep,
1331
- addDep: args.addDep,
1332
- });
1333
- if (result.error) return json({ error: result.error });
1334
-
1335
- return json({ ok: 1, updated: args.id });
1336
- },
1337
- }),
1338
-
1339
- bd_ready: tool({
1340
- description: "List ready-to-work tasks (no unresolved dependencies).",
1341
- args: {
1342
- sort: tool.schema.string().optional().describe("Sort field"),
1343
- limit: tool.schema.number().optional().describe("Max results"),
1344
- assignee: tool.schema
1345
- .string()
1346
- .optional()
1347
- .describe("Filter by assignee"),
1348
- label: tool.schema.string().optional().describe("Filter by label"),
1349
- },
1350
- async execute(args) {
1351
- const result = await bdReady({
1352
- sort: args.sort,
1353
- limit: args.limit,
1354
- assignee: args.assignee,
1355
- label: args.label,
1356
- });
1357
- if (result.error) return json({ error: result.error });
1358
-
1359
- return json({
1360
- items: result.tasks.map((t: Task) => ({
1361
- id: t.id,
1362
- title: t.title,
1363
- priority: t.priority,
1364
- })),
1365
- count: result.tasks.length,
1366
- });
1367
- },
1368
- }),
1369
-
1370
- bd_search: tool({
1371
- description: "Search issues by text query.",
1372
- args: {
1373
- query: tool.schema.string().describe("Search text"),
1374
- },
1375
- async execute(args) {
1376
- if (!args.query) return json({ error: "query required" });
1377
-
1378
- const { tasks, error } = await bdSearch(args.query);
1379
- if (error) return json({ error });
1380
-
1381
- return json({
1382
- items: tasks.map((t) => ({
1383
- id: t.id,
1384
- t: t.title,
1385
- p: t.priority,
1386
- s: t.status,
1387
- })),
1388
- count: tasks.length,
1389
- });
1390
- },
1391
- }),
1392
- },
1393
-
1394
- event: async ({ event }) => {
1395
- if (event.type === "session.idle" && state.currentTask) {
1396
- await bdSync();
1397
- }
1398
-
1399
- // Cleanup expired locks on compaction
1400
- if (event.type === "session.compacted") {
1401
- await cleanupExpiredLocks();
1402
- }
1403
-
1404
- // Log errors to audit trail
1405
- if (event.type === "session.error" && state.currentTask) {
1406
- await appendMessage({
1407
- id: `err-${Date.now().toString(36)}`,
1408
- from: state.agentId,
1409
- to: "all",
1410
- subj: `Session error on task ${state.currentTask}`,
1411
- body: `Error occurred during task execution`,
1412
- importance: "high",
1413
- at: Date.now(),
1414
- read: false,
1415
- });
1416
- }
1417
- },
1418
- };
1419
- };