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