opencodekit 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/template/.opencode/AGENTS.md +65 -27
- package/dist/template/.opencode/README.md +14 -19
- package/dist/template/.opencode/agent/build.md +4 -6
- package/dist/template/.opencode/agent/explore.md +18 -18
- package/dist/template/.opencode/agent/planner.md +4 -7
- package/dist/template/.opencode/agent/review.md +1 -2
- package/dist/template/.opencode/agent/rush.md +4 -6
- package/dist/template/.opencode/agent/scout.md +45 -38
- package/dist/template/.opencode/agent/vision.md +16 -24
- package/dist/template/.opencode/command/analyze-project.md +9 -9
- package/dist/template/.opencode/command/create.md +9 -4
- package/dist/template/.opencode/command/finish.md +12 -17
- package/dist/template/.opencode/command/fix-ci.md +10 -9
- package/dist/template/.opencode/command/fix-types.md +4 -11
- package/dist/template/.opencode/command/handoff.md +14 -18
- package/dist/template/.opencode/command/implement.md +11 -11
- package/dist/template/.opencode/command/import-plan.md +25 -14
- package/dist/template/.opencode/command/integration-test.md +1 -1
- package/dist/template/.opencode/command/issue.md +10 -9
- package/dist/template/.opencode/command/new-feature.md +4 -6
- package/dist/template/.opencode/command/plan.md +3 -5
- package/dist/template/.opencode/command/pr.md +2 -4
- package/dist/template/.opencode/command/research-and-implement.md +1 -1
- package/dist/template/.opencode/command/research.md +13 -15
- package/dist/template/.opencode/command/resume.md +2 -2
- package/dist/template/.opencode/command/revert-feature.md +5 -7
- package/dist/template/.opencode/command/status.md +8 -10
- package/dist/template/.opencode/dcp.jsonc +20 -2
- package/dist/template/.opencode/opencode.json +20 -35
- package/dist/template/.opencode/package.json +1 -1
- package/dist/template/.opencode/plugin/beads.ts +667 -0
- package/dist/template/.opencode/plugin/compaction.ts +80 -0
- package/dist/template/.opencode/plugin/skill-mcp.ts +458 -0
- package/dist/template/.opencode/skill/beads/SKILL.md +419 -0
- package/dist/template/.opencode/skill/beads/references/BOUNDARIES.md +218 -0
- package/dist/template/.opencode/skill/beads/references/DEPENDENCIES.md +130 -0
- package/dist/template/.opencode/skill/beads/references/RESUMABILITY.md +180 -0
- package/dist/template/.opencode/skill/beads/references/WORKFLOWS.md +222 -0
- package/dist/template/.opencode/skill/figma/SKILL.md +214 -0
- package/dist/template/.opencode/skill/playwright/SKILL.md +187 -0
- package/package.json +1 -1
|
@@ -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
|
+
};
|