opencodekit 0.6.7 → 0.8.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.
Files changed (62) hide show
  1. package/dist/index.js +654 -651
  2. package/dist/template/.opencode/AGENTS.md +97 -13
  3. package/dist/template/.opencode/README.md +18 -16
  4. package/dist/template/.opencode/command/accessibility-check.md +1 -1
  5. package/dist/template/.opencode/command/analyze-mockup.md +1 -1
  6. package/dist/template/.opencode/command/analyze-project.md +11 -9
  7. package/dist/template/.opencode/command/brainstorm.md +1 -1
  8. package/dist/template/.opencode/command/commit.md +1 -1
  9. package/dist/template/.opencode/command/create.md +16 -2
  10. package/dist/template/.opencode/command/design-audit.md +1 -1
  11. package/dist/template/.opencode/command/design.md +1 -1
  12. package/dist/template/.opencode/command/finish.md +20 -8
  13. package/dist/template/.opencode/command/fix-ci.md +14 -9
  14. package/dist/template/.opencode/command/fix-types.md +6 -11
  15. package/dist/template/.opencode/command/fix-ui.md +1 -1
  16. package/dist/template/.opencode/command/fix.md +1 -1
  17. package/dist/template/.opencode/command/handoff.md +8 -6
  18. package/dist/template/.opencode/command/implement.md +33 -3
  19. package/dist/template/.opencode/command/import-plan.md +27 -14
  20. package/dist/template/.opencode/command/integration-test.md +7 -3
  21. package/dist/template/.opencode/command/issue.md +10 -9
  22. package/dist/template/.opencode/command/new-feature.md +6 -6
  23. package/dist/template/.opencode/command/plan.md +5 -5
  24. package/dist/template/.opencode/command/pr.md +4 -4
  25. package/dist/template/.opencode/command/research-and-implement.md +2 -2
  26. package/dist/template/.opencode/command/research-ui.md +1 -1
  27. package/dist/template/.opencode/command/research.md +3 -5
  28. package/dist/template/.opencode/command/resume.md +4 -2
  29. package/dist/template/.opencode/command/revert-feature.md +7 -7
  30. package/dist/template/.opencode/command/review-codebase.md +1 -1
  31. package/dist/template/.opencode/command/skill-create.md +4 -4
  32. package/dist/template/.opencode/command/skill-optimize.md +4 -4
  33. package/dist/template/.opencode/command/status.md +4 -4
  34. package/dist/template/.opencode/command/ui-review.md +2 -2
  35. package/dist/template/.opencode/dcp.jsonc +20 -2
  36. package/dist/template/.opencode/opencode.json +496 -491
  37. package/dist/template/.opencode/package.json +20 -20
  38. package/dist/template/.opencode/plugin/beads.ts +667 -0
  39. package/dist/template/.opencode/plugin/compaction.ts +80 -0
  40. package/dist/template/.opencode/skill/beads/SKILL.md +419 -0
  41. package/dist/template/.opencode/skill/beads/references/BOUNDARIES.md +218 -0
  42. package/dist/template/.opencode/skill/beads/references/DEPENDENCIES.md +130 -0
  43. package/dist/template/.opencode/skill/beads/references/RESUMABILITY.md +180 -0
  44. package/dist/template/.opencode/skill/beads/references/WORKFLOWS.md +222 -0
  45. package/dist/template/.opencode/skill/brainstorming/SKILL.md +2 -2
  46. package/dist/template/.opencode/skill/executing-plans/SKILL.md +1 -1
  47. package/dist/template/.opencode/skill/sharing-skills/SKILL.md +13 -4
  48. package/dist/template/.opencode/skill/subagent-driven-development/SKILL.md +1 -1
  49. package/dist/template/.opencode/skill/systematic-debugging/SKILL.md +2 -2
  50. package/dist/template/.opencode/skill/using-git-worktrees/SKILL.md +27 -18
  51. package/dist/template/.opencode/skill/{using-superpowers → using-skills}/SKILL.md +6 -3
  52. package/dist/template/.opencode/skill/writing-plans/SKILL.md +3 -3
  53. package/dist/template/.opencode/skill/writing-skills/SKILL.md +2 -2
  54. package/package.json +2 -1
  55. package/dist/template/.opencode/memory/handoffs/2025-12-27T103000Z.md +0 -76
  56. package/dist/template/.opencode/plugin/skill.ts +0 -275
  57. package/dist/template/.opencode/skill/systematic-debugging/CREATION-LOG.md +0 -119
  58. package/dist/template/.opencode/skill/systematic-debugging/test-academic.md +0 -14
  59. package/dist/template/.opencode/skill/systematic-debugging/test-pressure-1.md +0 -58
  60. package/dist/template/.opencode/skill/systematic-debugging/test-pressure-2.md +0 -68
  61. package/dist/template/.opencode/skill/systematic-debugging/test-pressure-3.md +0 -69
  62. package/dist/template/.opencode/skill/testing-skills-with-subagents/examples/CLAUDE_MD_TESTING.md +0 -189
@@ -1,22 +1,22 @@
1
1
  {
2
- "name": "opencode",
3
- "version": "1.0.0",
4
- "description": "Opencode plugin",
5
- "type": "module",
6
- "main": "index.js",
7
- "scripts": {
8
- "type-check": "tsc --noEmit"
9
- },
10
- "keywords": [],
11
- "author": "",
12
- "license": "ISC",
13
- "dependencies": {
14
- "@opencode-ai/plugin": "1.0.207"
15
- },
16
- "devDependencies": {
17
- "@types/node": "^25.0.3",
18
- "fs": "^0.0.1-security",
19
- "path": "^0.12.7",
20
- "typescript": "^5.9.3"
21
- }
2
+ "name": "opencode",
3
+ "version": "1.0.0",
4
+ "description": "Opencode plugin",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "scripts": {
8
+ "type-check": "tsc --noEmit"
9
+ },
10
+ "keywords": [],
11
+ "author": "",
12
+ "license": "ISC",
13
+ "dependencies": {
14
+ "@opencode-ai/plugin": "1.0.222"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^25.0.3",
18
+ "fs": "^0.0.1-security",
19
+ "path": "^0.12.7",
20
+ "typescript": "^5.9.3"
21
+ }
22
22
  }
@@ -0,0 +1,667 @@
1
+ import { type Plugin, tool } from "@opencode-ai/plugin";
2
+
3
+ interface TaskState {
4
+ currentTask: string | null;
5
+ reservedFiles: Set<string>;
6
+ team: string;
7
+ role: string;
8
+ initialized: boolean;
9
+ agentId: string;
10
+ }
11
+
12
+ export const BeadsCorePlugin: Plugin = async ({ $, directory }) => {
13
+ const state: TaskState = {
14
+ currentTask: null,
15
+ reservedFiles: new Set(),
16
+ team: "default",
17
+ role: "",
18
+ initialized: false,
19
+ agentId: `agent-${Date.now().toString(36)}`,
20
+ };
21
+
22
+ async function bd(...args: string[]): Promise<any> {
23
+ try {
24
+ const result = await $`bd ${args}`.text();
25
+ try {
26
+ return JSON.parse(result);
27
+ } catch {
28
+ return { output: result.trim() };
29
+ }
30
+ } catch (e: any) {
31
+ return { error: e.message || String(e) };
32
+ }
33
+ }
34
+
35
+ function j(data: any): string {
36
+ return JSON.stringify(data, null, 0);
37
+ }
38
+
39
+ function reservationPath(filePath: string): string {
40
+ const safe = filePath.replace(/[\/\\]/g, "_").replace(/\.\./g, "_");
41
+ return `.reservations/${safe}.lock`;
42
+ }
43
+
44
+ async function getAllReservations(): Promise<any[]> {
45
+ try {
46
+ const dir = ".reservations";
47
+ const result = await $`test -d ${dir} && ls -1 ${dir}`
48
+ .text()
49
+ .catch(() => "");
50
+ if (!result.trim()) return [];
51
+
52
+ const reservations: any[] = [];
53
+ for (const file of result.trim().split("\n")) {
54
+ if (!file.endsWith(".lock")) continue;
55
+ 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);
60
+ }
61
+ } catch {}
62
+ }
63
+ return reservations;
64
+ } catch {
65
+ return [];
66
+ }
67
+ }
68
+
69
+ return {
70
+ tool: {
71
+ bd_init: tool({
72
+ description: "Join beads workspace. MUST call first.",
73
+ args: {
74
+ team: tool.schema.string().optional().describe("Team name"),
75
+ role: tool.schema
76
+ .string()
77
+ .optional()
78
+ .describe("Role: fe|be|mobile|devops|qa"),
79
+ },
80
+ async execute(args) {
81
+ await $`bd init`.quiet().catch(() => {});
82
+ await $`mkdir -p .reservations`.quiet();
83
+
84
+ state.team = args.team || "default";
85
+ state.role = args.role || "";
86
+ state.initialized = true;
87
+ state.currentTask = null;
88
+ state.reservedFiles.clear();
89
+
90
+ return j({
91
+ ok: 1,
92
+ agent: state.agentId,
93
+ ws: directory,
94
+ team: state.team,
95
+ role: state.role || undefined,
96
+ });
97
+ },
98
+ }),
99
+
100
+ bd_claim: tool({
101
+ description: "Claim next ready task. Auto-syncs, marks in_progress.",
102
+ args: {},
103
+ async execute() {
104
+ if (!state.initialized) return j({ error: "Call bd_init() first" });
105
+
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" });
112
+
113
+ let task = tasks[0];
114
+ if (state.role) {
115
+ const roleTask = tasks.find((t: any) =>
116
+ t.tags?.includes(state.role),
117
+ );
118
+ if (roleTask) task = roleTask;
119
+ }
120
+
121
+ await $`bd update ${task.id} --status in_progress`.quiet();
122
+ state.currentTask = task.id;
123
+
124
+ return j({
125
+ id: task.id,
126
+ t: task.title,
127
+ p: task.priority,
128
+ type: task.type,
129
+ });
130
+ },
131
+ }),
132
+
133
+ bd_done: tool({
134
+ description:
135
+ "Complete task. Auto-releases files, syncs. Restart session after.",
136
+ args: {
137
+ id: tool.schema.string().describe("Task ID"),
138
+ msg: tool.schema
139
+ .string()
140
+ .default("completed")
141
+ .describe("Completion message"),
142
+ },
143
+ async execute(args) {
144
+ const taskId = args.id || state.currentTask;
145
+ if (!taskId) return j({ error: "No task ID" });
146
+
147
+ await bd("close", taskId, "--reason", args.msg);
148
+
149
+ for (const path of state.reservedFiles) {
150
+ await $`rm -f ${reservationPath(path)}`.quiet().catch(() => {});
151
+ }
152
+ state.reservedFiles.clear();
153
+
154
+ await $`bd sync`.quiet().catch(() => {});
155
+ state.currentTask = null;
156
+
157
+ return j({ ok: 1, closed: taskId, hint: "Restart session" });
158
+ },
159
+ }),
160
+
161
+ bd_add: tool({
162
+ description: "Create issue. Use tags to assign to roles.",
163
+ args: {
164
+ title: tool.schema.string().describe("Actionable title"),
165
+ desc: tool.schema
166
+ .string()
167
+ .optional()
168
+ .describe("Why/what/how context"),
169
+ pri: tool.schema
170
+ .number()
171
+ .default(2)
172
+ .describe("0=critical,1=high,2=normal,3=low,4=backlog"),
173
+ type: tool.schema
174
+ .string()
175
+ .default("task")
176
+ .describe("task|bug|feature|epic|chore"),
177
+ tags: tool.schema
178
+ .array(tool.schema.string())
179
+ .optional()
180
+ .describe("Role tags: fe,be,mobile,devops,qa"),
181
+ parent: tool.schema.string().optional().describe("Parent issue ID"),
182
+ deps: tool.schema
183
+ .array(tool.schema.string())
184
+ .optional()
185
+ .describe("Dependencies (type:id format)"),
186
+ },
187
+ 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,
202
+ t: args.title,
203
+ p: args.pri || 2,
204
+ });
205
+ },
206
+ }),
207
+
208
+ bd_assign: tool({
209
+ description:
210
+ "Assign task to role (leader only). Adds tag and notifies.",
211
+ args: {
212
+ id: tool.schema.string().describe("Issue ID"),
213
+ role: tool.schema.string().describe("Role: fe|be|mobile|devops|qa"),
214
+ notify: tool.schema
215
+ .boolean()
216
+ .default(true)
217
+ .describe("Broadcast notification"),
218
+ },
219
+ async execute(args) {
220
+ if (!args.id || !args.role)
221
+ return j({ error: "id and role required" });
222
+
223
+ await bd("update", args.id, "--tags", args.role);
224
+
225
+ if (args.notify !== false) {
226
+ const notif = {
227
+ type: "assign",
228
+ task: args.id,
229
+ role: args.role,
230
+ at: Date.now(),
231
+ };
232
+ await $`echo ${JSON.stringify(notif)} >> .reservations/notifications.jsonl`.quiet();
233
+ }
234
+
235
+ return j({ ok: 1, id: args.id, role: args.role });
236
+ },
237
+ }),
238
+
239
+ bd_ls: tool({
240
+ description: "List issues. status: open|closed|in_progress|ready|all",
241
+ args: {
242
+ status: tool.schema.string().default("open"),
243
+ limit: tool.schema.number().default(10),
244
+ offset: tool.schema.number().default(0),
245
+ },
246
+ async execute(args) {
247
+ const status = args.status || "open";
248
+ const limit = Math.min(args.limit || 10, 50);
249
+
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
+ }
258
+
259
+ if (result.error) return j({ error: result.error });
260
+
261
+ const tasks = Array.isArray(result) ? result : [];
262
+ const offset = args.offset || 0;
263
+ const items = tasks.slice(offset, offset + limit).map((t: any) => ({
264
+ id: t.id,
265
+ t: t.title,
266
+ p: t.priority,
267
+ s: t.status,
268
+ tags: t.tags?.length ? t.tags : undefined,
269
+ }));
270
+
271
+ return j({ items, count: items.length, total: tasks.length });
272
+ },
273
+ }),
274
+
275
+ bd_show: tool({
276
+ description: "Get full issue details.",
277
+ args: { id: tool.schema.string().describe("Issue ID") },
278
+ 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);
282
+ },
283
+ }),
284
+
285
+ bd_reserve: tool({
286
+ description: "Lock files for editing. Prevents conflicts.",
287
+ args: {
288
+ paths: tool.schema
289
+ .array(tool.schema.string())
290
+ .describe("Files to lock"),
291
+ reason: tool.schema.string().optional().describe("Why reserving"),
292
+ ttl: tool.schema
293
+ .number()
294
+ .default(600)
295
+ .describe("Seconds until expiry"),
296
+ },
297
+ async execute(args) {
298
+ if (!args.paths?.length) return j({ error: "paths required" });
299
+
300
+ await $`mkdir -p .reservations`.quiet();
301
+
302
+ const granted: string[] = [];
303
+ const conflicts: { path: string; holder?: string }[] = [];
304
+ const now = Date.now();
305
+ const expires = now + (args.ttl || 600) * 1000;
306
+
307
+ 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);
333
+ }
334
+
335
+ const result: any = { granted };
336
+ if (conflicts.length) result.conflicts = conflicts;
337
+ return j(result);
338
+ },
339
+ }),
340
+
341
+ bd_release: tool({
342
+ description: "Unlock files. Auto-released on done().",
343
+ args: {
344
+ paths: tool.schema
345
+ .array(tool.schema.string())
346
+ .optional()
347
+ .describe("Files to unlock (empty=all)"),
348
+ },
349
+ async execute(args) {
350
+ const toRelease = args.paths?.length
351
+ ? args.paths
352
+ : [...state.reservedFiles];
353
+
354
+ for (const path of toRelease) {
355
+ await $`rm -f ${reservationPath(path)}`.quiet().catch(() => {});
356
+ state.reservedFiles.delete(path);
357
+ }
358
+
359
+ return j({ released: toRelease });
360
+ },
361
+ }),
362
+
363
+ bd_reservations: tool({
364
+ description: "List active file locks. Check before editing.",
365
+ args: {},
366
+ async execute() {
367
+ const reservations = await getAllReservations();
368
+ return j({
369
+ locks: reservations.map((r) => ({
370
+ path: r.path,
371
+ agent: r.agent,
372
+ expires: new Date(r.expires).toISOString(),
373
+ task: r.task,
374
+ })),
375
+ count: reservations.length,
376
+ });
377
+ },
378
+ }),
379
+
380
+ bd_msg: tool({
381
+ description:
382
+ "Send message. Use global=true + to='all' for team-wide broadcast.",
383
+ args: {
384
+ subj: tool.schema.string().describe("Subject"),
385
+ body: tool.schema.string().optional().describe("Message body"),
386
+ to: tool.schema
387
+ .string()
388
+ .default("all")
389
+ .describe("Recipient or 'all'"),
390
+ importance: tool.schema
391
+ .string()
392
+ .default("normal")
393
+ .describe("low|normal|high"),
394
+ thread: tool.schema.string().optional().describe("Thread ID"),
395
+ global: tool.schema
396
+ .boolean()
397
+ .default(false)
398
+ .describe("Send to all workspaces"),
399
+ },
400
+ async execute(args) {
401
+ if (!args.subj) return j({ error: "subj required" });
402
+
403
+ const msg = {
404
+ id: `msg-${Date.now().toString(36)}`,
405
+ from: state.agentId,
406
+ to: args.to || "all",
407
+ subj: args.subj,
408
+ body: args.body,
409
+ importance: args.importance || "normal",
410
+ thread: args.thread,
411
+ global: args.global,
412
+ at: Date.now(),
413
+ read: false,
414
+ };
415
+
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 });
420
+ },
421
+ }),
422
+
423
+ bd_inbox: tool({
424
+ description: "Get messages. Includes global by default.",
425
+ args: {
426
+ n: tool.schema.number().default(5).describe("Max messages"),
427
+ unread: tool.schema.boolean().default(false).describe("Unread only"),
428
+ global: tool.schema
429
+ .boolean()
430
+ .default(true)
431
+ .describe("Include cross-workspace"),
432
+ },
433
+ 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
+ }
459
+ },
460
+ }),
461
+
462
+ bd_status: tool({
463
+ description: "Workspace overview. Shows agents, tasks, locks.",
464
+ args: {
465
+ include_agents: tool.schema
466
+ .boolean()
467
+ .default(false)
468
+ .describe("Include agent info"),
469
+ },
470
+ async execute(args) {
471
+ const [ready, inProgress, reservations] = await Promise.all([
472
+ bd("ready", "--json"),
473
+ bd("list", "--status", "in_progress", "--json"),
474
+ getAllReservations(),
475
+ ]);
476
+
477
+ const result: any = {
478
+ ws: directory,
479
+ team: state.team,
480
+ 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,
484
+ };
485
+
486
+ if (args.include_agents) {
487
+ result.agent = {
488
+ id: state.agentId,
489
+ role: state.role || undefined,
490
+ reserved: [...state.reservedFiles],
491
+ };
492
+ }
493
+
494
+ return j(result);
495
+ },
496
+ }),
497
+
498
+ bd_sync: tool({
499
+ description: "Sync with git. Pull/push changes.",
500
+ args: {},
501
+ async execute() {
502
+ const result = await bd("sync");
503
+ return j({ ok: 1, output: result.output });
504
+ },
505
+ }),
506
+
507
+ bd_cleanup: tool({
508
+ description: "Remove old closed issues. Run every few days.",
509
+ args: {
510
+ days: tool.schema
511
+ .number()
512
+ .default(2)
513
+ .describe("Delete closed >N days"),
514
+ },
515
+ 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 });
522
+ },
523
+ }),
524
+
525
+ bd_doctor: tool({
526
+ description: "Check/repair database health.",
527
+ args: {},
528
+ async execute() {
529
+ const result = await bd("doctor", "--fix");
530
+ return j({ ok: 1, output: result.output });
531
+ },
532
+ }),
533
+
534
+ bd_insights: tool({
535
+ description:
536
+ "Graph analysis: bottlenecks, keystones, cycles, PageRank.",
537
+ args: {},
538
+ 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) => ({
556
+ id: t.id,
557
+ t: t.title,
558
+ p: t.priority,
559
+ })),
560
+ });
561
+ },
562
+ }),
563
+
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
+ }
577
+
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
+ });
585
+ },
586
+ }),
587
+
588
+ bd_priority: tool({
589
+ description: "Priority recommendations based on graph analysis.",
590
+ args: {
591
+ limit: tool.schema
592
+ .number()
593
+ .default(5)
594
+ .describe("Max issues to return"),
595
+ },
596
+ async execute(args) {
597
+ const ready = await bd("ready", "--json");
598
+ const tasks = Array.isArray(ready) ? ready : [];
599
+
600
+ const sorted = tasks
601
+ .sort((a: any, b: any) => (a.priority || 2) - (b.priority || 2))
602
+ .slice(0, args.limit || 5);
603
+
604
+ return j({
605
+ recommended: sorted.map((t: any) => ({
606
+ 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",
615
+ })),
616
+ });
617
+ },
618
+ }),
619
+
620
+ bd_diff: tool({
621
+ description: "Compare issue changes between git revisions.",
622
+ 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)"),
631
+ },
632
+ async execute(args) {
633
+ const sinceArg = args.since || "HEAD~10";
634
+ const asOfArg = args.as_of || "HEAD";
635
+
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
+ }
657
+ },
658
+ }),
659
+ },
660
+
661
+ event: async ({ event }) => {
662
+ if (event.type === "session.idle" && state.currentTask) {
663
+ await $`bd sync`.quiet().catch(() => {});
664
+ }
665
+ },
666
+ };
667
+ };