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