opencodekit 0.17.7 → 0.17.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/template/.opencode/AGENTS.md +2 -1
- package/dist/template/.opencode/agent/general.md +1 -1
- package/dist/template/.opencode/agent/painter.md +1 -1
- package/dist/template/.opencode/agent/vision.md +1 -1
- package/dist/template/.opencode/opencode.json +2 -5
- package/dist/template/.opencode/package.json +1 -1
- package/dist/template/.opencode/plugin/sessions.ts +71 -266
- package/package.json +1 -1
- package/dist/template/.opencode/agent/looker.md +0 -102
- package/dist/template/.opencode/plugin/hashline.ts.bak +0 -757
- package/dist/template/.opencode/plugin/swarm-enforcer.ts +0 -371
- package/dist/template/.opencode/tool/swarm.ts +0 -605
|
@@ -1,605 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
|
-
import { tool } from "@opencode-ai/plugin";
|
|
6
|
-
|
|
7
|
-
const execFileAsync = promisify(execFile);
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Unified swarm orchestration tool.
|
|
11
|
-
* Consolidates: swarm-plan, swarm-monitor, swarm-delegate, beads-sync
|
|
12
|
-
*/
|
|
13
|
-
export default tool({
|
|
14
|
-
description: `Swarm orchestration for parallel task execution.
|
|
15
|
-
|
|
16
|
-
Operations (choose by op):
|
|
17
|
-
- plan: Analyze task for parallel execution
|
|
18
|
-
Examples:
|
|
19
|
-
swarm({ op: "plan", task: "Investigate auth failures", files: "src/auth.ts,src/api.ts" })
|
|
20
|
-
swarm({ op: "plan", task: "Refactor utils" })
|
|
21
|
-
- monitor: Track worker progress
|
|
22
|
-
Examples:
|
|
23
|
-
swarm({ op: "monitor", team: "frontend", action: "status" })
|
|
24
|
-
swarm({ op: "monitor", team: "frontend", action: "update", worker_id: "w1", phase: "build", progress: 60, status: "working" })
|
|
25
|
-
- delegate: Create delegation packet
|
|
26
|
-
Examples:
|
|
27
|
-
swarm({ op: "delegate", bead_id: "B-123", title: "Add caching", outcome: "Cache layer in place", checks: "lint,typecheck" })
|
|
28
|
-
swarm({ op: "delegate", bead_id: "B-123", outcome: "Bug fixed", must_do: "add test", must_not: "change API" })
|
|
29
|
-
- sync: Bridge Beads tasks to OpenCode todos
|
|
30
|
-
Examples:
|
|
31
|
-
swarm({ op: "sync", action: "push", filter: "open" })
|
|
32
|
-
swarm({ op: "sync", action: "pull" })
|
|
33
|
-
|
|
34
|
-
Tip: Each example only applies when the matching op is used.`,
|
|
35
|
-
|
|
36
|
-
args: {
|
|
37
|
-
op: tool.schema
|
|
38
|
-
.enum(["plan", "monitor", "delegate", "sync"])
|
|
39
|
-
.describe("Operation: plan, monitor, delegate, sync"),
|
|
40
|
-
// Plan args
|
|
41
|
-
task: tool.schema.string().optional().describe("Task description (plan)"),
|
|
42
|
-
files: tool.schema
|
|
43
|
-
.string()
|
|
44
|
-
.optional()
|
|
45
|
-
.describe("Comma-separated files (plan)"),
|
|
46
|
-
// Monitor args
|
|
47
|
-
team: tool.schema.string().optional().describe("Team name (monitor)"),
|
|
48
|
-
action: tool.schema
|
|
49
|
-
.enum(["update", "render", "status", "clear", "push", "pull"])
|
|
50
|
-
.optional()
|
|
51
|
-
.describe(
|
|
52
|
-
"Monitor action: update, render, status, clear | Sync action: push, pull",
|
|
53
|
-
),
|
|
54
|
-
worker_id: tool.schema.string().optional().describe("Worker ID (monitor)"),
|
|
55
|
-
phase: tool.schema.string().optional().describe("Phase name (monitor)"),
|
|
56
|
-
progress: tool.schema
|
|
57
|
-
.number()
|
|
58
|
-
.min(0)
|
|
59
|
-
.max(100)
|
|
60
|
-
.optional()
|
|
61
|
-
.describe("Progress 0-100 (monitor)"),
|
|
62
|
-
status: tool.schema.string().optional().describe("Worker status (monitor)"),
|
|
63
|
-
file: tool.schema.string().optional().describe("Current file (monitor)"),
|
|
64
|
-
// Delegate args
|
|
65
|
-
bead_id: tool.schema.string().optional().describe("Bead ID (delegate)"),
|
|
66
|
-
title: tool.schema.string().optional().describe("Task title (delegate)"),
|
|
67
|
-
outcome: tool.schema
|
|
68
|
-
.string()
|
|
69
|
-
.optional()
|
|
70
|
-
.describe("Expected outcome (delegate)"),
|
|
71
|
-
must_do: tool.schema
|
|
72
|
-
.string()
|
|
73
|
-
.optional()
|
|
74
|
-
.describe("Must do list (delegate)"),
|
|
75
|
-
must_not: tool.schema
|
|
76
|
-
.string()
|
|
77
|
-
.optional()
|
|
78
|
-
.describe("Must not do list (delegate)"),
|
|
79
|
-
checks: tool.schema
|
|
80
|
-
.string()
|
|
81
|
-
.optional()
|
|
82
|
-
.describe("Acceptance checks (delegate)"),
|
|
83
|
-
context: tool.schema
|
|
84
|
-
.string()
|
|
85
|
-
.optional()
|
|
86
|
-
.describe("Extra context (delegate)"),
|
|
87
|
-
write: tool.schema
|
|
88
|
-
.boolean()
|
|
89
|
-
.optional()
|
|
90
|
-
.describe("Write to file (delegate)"),
|
|
91
|
-
// Sync args
|
|
92
|
-
filter: tool.schema
|
|
93
|
-
.enum(["open", "in_progress", "all"])
|
|
94
|
-
.optional()
|
|
95
|
-
.describe("Filter: open, in_progress, all (sync)"),
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
execute: async (args, ctx) => {
|
|
99
|
-
const worktree = ctx.worktree || process.cwd();
|
|
100
|
-
|
|
101
|
-
switch (args.op) {
|
|
102
|
-
case "plan":
|
|
103
|
-
return planOperation(args.task || "", args.files);
|
|
104
|
-
case "monitor":
|
|
105
|
-
return monitorOperation(args, worktree);
|
|
106
|
-
case "delegate":
|
|
107
|
-
return delegateOperation(args, worktree);
|
|
108
|
-
case "sync":
|
|
109
|
-
return syncOperation(args, worktree);
|
|
110
|
-
default:
|
|
111
|
-
return `Error: Unknown operation: ${args.op}`;
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// ============================================================
|
|
117
|
-
// PLAN OPERATION (from swarm-plan.ts)
|
|
118
|
-
// ============================================================
|
|
119
|
-
|
|
120
|
-
interface TaskClassification {
|
|
121
|
-
type: "search" | "batch" | "writing" | "sequential" | "mixed";
|
|
122
|
-
coupling: "high" | "medium" | "low";
|
|
123
|
-
recommended_agents: number;
|
|
124
|
-
reasoning: string;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function planOperation(task: string, files?: string): string {
|
|
128
|
-
const fileList = files?.split(",").filter(Boolean) || [];
|
|
129
|
-
const fileCount = Number.parseInt(files || "0") || fileList.length;
|
|
130
|
-
|
|
131
|
-
const classification = classifyTask(task, fileList);
|
|
132
|
-
const collapseCheck = detectSerialCollapse(
|
|
133
|
-
task,
|
|
134
|
-
fileCount,
|
|
135
|
-
classification.recommended_agents,
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
let recommendation: string;
|
|
139
|
-
if (collapseCheck.is_collapse) {
|
|
140
|
-
recommendation = `Swarm: ${Math.min(fileCount, 5)} agents (serial collapse detected)`;
|
|
141
|
-
} else if (classification.recommended_agents > 1) {
|
|
142
|
-
recommendation = `Swarm: ${classification.recommended_agents} agents`;
|
|
143
|
-
} else {
|
|
144
|
-
recommendation = "Single agent sufficient";
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return JSON.stringify(
|
|
148
|
-
{
|
|
149
|
-
task: task.slice(0, 100),
|
|
150
|
-
file_count: fileCount,
|
|
151
|
-
classification,
|
|
152
|
-
serial_collapse: collapseCheck,
|
|
153
|
-
recommendation,
|
|
154
|
-
},
|
|
155
|
-
null,
|
|
156
|
-
2,
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function classifyTask(task: string, files: string[]): TaskClassification {
|
|
161
|
-
const searchPatterns = /research|find|search|explore|investigate/i;
|
|
162
|
-
const batchPatterns = /refactor|update|migrate|convert.*all|batch/i;
|
|
163
|
-
const sequentialPatterns = /debug|fix.*issue|optimize|complex/i;
|
|
164
|
-
|
|
165
|
-
const coupling = analyzeCoupling(files);
|
|
166
|
-
|
|
167
|
-
if (searchPatterns.test(task)) {
|
|
168
|
-
return {
|
|
169
|
-
type: "search",
|
|
170
|
-
coupling: "low",
|
|
171
|
-
recommended_agents: Math.min(Math.max(files.length, 3), 5),
|
|
172
|
-
reasoning: "Search tasks benefit from parallel exploration",
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if ((batchPatterns.test(task) || files.length > 3) && files.length > 0) {
|
|
177
|
-
return {
|
|
178
|
-
type: "batch",
|
|
179
|
-
coupling,
|
|
180
|
-
recommended_agents: Math.min(files.length, 8),
|
|
181
|
-
reasoning: `Batch processing ${files.length} files`,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
sequentialPatterns.test(task) ||
|
|
187
|
-
coupling === "high" ||
|
|
188
|
-
files.length <= 2
|
|
189
|
-
) {
|
|
190
|
-
return {
|
|
191
|
-
type: "sequential",
|
|
192
|
-
coupling: "high",
|
|
193
|
-
recommended_agents: 1,
|
|
194
|
-
reasoning: "High coupling requires sequential execution",
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
type: "mixed",
|
|
200
|
-
coupling,
|
|
201
|
-
recommended_agents: Math.min(files.length || 2, 4),
|
|
202
|
-
reasoning: "Mixed approach with verification",
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function analyzeCoupling(files: string[]): "high" | "medium" | "low" {
|
|
207
|
-
if (files.length <= 1) return "high";
|
|
208
|
-
if (files.length <= 3) return "medium";
|
|
209
|
-
const dirs = files.map((f) => path.dirname(f));
|
|
210
|
-
const uniqueDirs = new Set(dirs);
|
|
211
|
-
if (uniqueDirs.size === 1) return "high";
|
|
212
|
-
if (uniqueDirs.size <= files.length / 2) return "medium";
|
|
213
|
-
return "low";
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function detectSerialCollapse(
|
|
217
|
-
task: string,
|
|
218
|
-
fileCount: number,
|
|
219
|
-
agents: number,
|
|
220
|
-
): { is_collapse: boolean; warnings: string[] } {
|
|
221
|
-
const warnings: string[] = [];
|
|
222
|
-
if (fileCount >= 5 && agents === 1) warnings.push("Many files, single agent");
|
|
223
|
-
if (/research|search/i.test(task) && agents === 1)
|
|
224
|
-
warnings.push("Search with single agent");
|
|
225
|
-
if (/refactor.*all|update.*all/i.test(task) && agents === 1)
|
|
226
|
-
warnings.push("Batch with single agent");
|
|
227
|
-
|
|
228
|
-
return {
|
|
229
|
-
is_collapse:
|
|
230
|
-
warnings.length >= 2 || (warnings.length === 1 && fileCount > 8),
|
|
231
|
-
warnings,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// ============================================================
|
|
236
|
-
// MONITOR OPERATION (from swarm-monitor.ts)
|
|
237
|
-
// ============================================================
|
|
238
|
-
|
|
239
|
-
const PROGRESS_FILE = ".beads/swarm-progress.jsonl";
|
|
240
|
-
|
|
241
|
-
interface ProgressEntry {
|
|
242
|
-
timestamp: string;
|
|
243
|
-
team_name: string;
|
|
244
|
-
worker_id: string;
|
|
245
|
-
phase: string;
|
|
246
|
-
progress: number;
|
|
247
|
-
status: string;
|
|
248
|
-
file?: string;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
type MonitorAction = "update" | "render" | "status" | "clear";
|
|
252
|
-
type SyncAction = "push" | "pull";
|
|
253
|
-
type SyncFilter = "open" | "in_progress" | "all";
|
|
254
|
-
|
|
255
|
-
const MONITOR_ACTIONS = new Set<MonitorAction>([
|
|
256
|
-
"update",
|
|
257
|
-
"render",
|
|
258
|
-
"status",
|
|
259
|
-
"clear",
|
|
260
|
-
]);
|
|
261
|
-
|
|
262
|
-
async function monitorOperation(
|
|
263
|
-
args: {
|
|
264
|
-
team?: string;
|
|
265
|
-
action?: MonitorAction | SyncAction;
|
|
266
|
-
worker_id?: string;
|
|
267
|
-
phase?: string;
|
|
268
|
-
progress?: number;
|
|
269
|
-
status?: string;
|
|
270
|
-
file?: string;
|
|
271
|
-
},
|
|
272
|
-
worktree: string,
|
|
273
|
-
): Promise<string> {
|
|
274
|
-
const team = args.team || "default";
|
|
275
|
-
if (args.action && !MONITOR_ACTIONS.has(args.action as MonitorAction)) {
|
|
276
|
-
return `Invalid monitor action: ${args.action}`;
|
|
277
|
-
}
|
|
278
|
-
const action: MonitorAction = (args.action as MonitorAction) || "status";
|
|
279
|
-
|
|
280
|
-
switch (action) {
|
|
281
|
-
case "update":
|
|
282
|
-
return updateProgress(
|
|
283
|
-
{
|
|
284
|
-
timestamp: new Date().toISOString(),
|
|
285
|
-
team_name: team,
|
|
286
|
-
worker_id: args.worker_id || "unknown",
|
|
287
|
-
phase: args.phase || "unknown",
|
|
288
|
-
progress: args.progress || 0,
|
|
289
|
-
status: args.status || "idle",
|
|
290
|
-
file: args.file,
|
|
291
|
-
},
|
|
292
|
-
worktree,
|
|
293
|
-
);
|
|
294
|
-
case "render":
|
|
295
|
-
return renderProgress(team, worktree);
|
|
296
|
-
case "status":
|
|
297
|
-
return getFullStatus(team, worktree);
|
|
298
|
-
case "clear":
|
|
299
|
-
return clearTeam(team, worktree);
|
|
300
|
-
default:
|
|
301
|
-
return `Unknown action: ${action}`;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async function updateProgress(
|
|
306
|
-
entry: ProgressEntry,
|
|
307
|
-
worktree: string,
|
|
308
|
-
): Promise<string> {
|
|
309
|
-
const progressPath = path.join(worktree, PROGRESS_FILE);
|
|
310
|
-
await fs.mkdir(path.dirname(progressPath), { recursive: true });
|
|
311
|
-
await fs.appendFile(progressPath, `${JSON.stringify(entry)}\n`, "utf-8");
|
|
312
|
-
return JSON.stringify({ success: true, record: entry }, null, 2);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async function getProgress(
|
|
316
|
-
team: string,
|
|
317
|
-
worktree: string,
|
|
318
|
-
): Promise<{ workers: ProgressEntry[] }> {
|
|
319
|
-
const progressPath = path.join(worktree, PROGRESS_FILE);
|
|
320
|
-
try {
|
|
321
|
-
const content = await fs.readFile(progressPath, "utf-8");
|
|
322
|
-
const entries = content
|
|
323
|
-
.trim()
|
|
324
|
-
.split("\n")
|
|
325
|
-
.filter(Boolean)
|
|
326
|
-
.map((l) => JSON.parse(l) as ProgressEntry)
|
|
327
|
-
.filter((e) => e.team_name === team);
|
|
328
|
-
const workerMap = new Map<string, ProgressEntry>();
|
|
329
|
-
for (const e of entries) workerMap.set(e.worker_id, e);
|
|
330
|
-
return { workers: Array.from(workerMap.values()) };
|
|
331
|
-
} catch {
|
|
332
|
-
return { workers: [] };
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
async function renderProgress(team: string, worktree: string): Promise<string> {
|
|
337
|
-
const { workers } = await getProgress(team, worktree);
|
|
338
|
-
if (workers.length === 0) return `No progress for team: ${team}`;
|
|
339
|
-
|
|
340
|
-
let output = `## Swarm: ${team}\n\n| Worker | Phase | Progress | Status |\n|---|---|---|---|\n`;
|
|
341
|
-
for (const w of workers) {
|
|
342
|
-
output += `| ${w.worker_id} | ${w.phase} | ${w.progress}% | ${w.status} |\n`;
|
|
343
|
-
}
|
|
344
|
-
return output;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
async function getFullStatus(team: string, worktree: string): Promise<string> {
|
|
348
|
-
const { workers } = await getProgress(team, worktree);
|
|
349
|
-
return JSON.stringify(
|
|
350
|
-
{
|
|
351
|
-
team,
|
|
352
|
-
workers: workers.length,
|
|
353
|
-
completed: workers.filter((w) => w.status === "completed").length,
|
|
354
|
-
working: workers.filter((w) => w.status === "working").length,
|
|
355
|
-
errors: workers.filter((w) => w.status === "error").length,
|
|
356
|
-
details: workers,
|
|
357
|
-
},
|
|
358
|
-
null,
|
|
359
|
-
2,
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
async function clearTeam(team: string, worktree: string): Promise<string> {
|
|
364
|
-
const progressPath = path.join(worktree, PROGRESS_FILE);
|
|
365
|
-
try {
|
|
366
|
-
const content = await fs.readFile(progressPath, "utf-8");
|
|
367
|
-
const entries = content
|
|
368
|
-
.trim()
|
|
369
|
-
.split("\n")
|
|
370
|
-
.filter(Boolean)
|
|
371
|
-
.map((l) => JSON.parse(l) as ProgressEntry);
|
|
372
|
-
const other = entries.filter((e) => e.team_name !== team);
|
|
373
|
-
await fs.writeFile(
|
|
374
|
-
progressPath,
|
|
375
|
-
other.map((e) => JSON.stringify(e)).join("\n") +
|
|
376
|
-
(other.length ? "\n" : ""),
|
|
377
|
-
"utf-8",
|
|
378
|
-
);
|
|
379
|
-
return JSON.stringify({
|
|
380
|
-
success: true,
|
|
381
|
-
cleared: entries.length - other.length,
|
|
382
|
-
});
|
|
383
|
-
} catch {
|
|
384
|
-
return JSON.stringify({ success: true, cleared: 0 });
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// ============================================================
|
|
389
|
-
// DELEGATE OPERATION (from swarm-delegate.ts)
|
|
390
|
-
// ============================================================
|
|
391
|
-
|
|
392
|
-
async function delegateOperation(
|
|
393
|
-
args: {
|
|
394
|
-
bead_id?: string;
|
|
395
|
-
title?: string;
|
|
396
|
-
outcome?: string;
|
|
397
|
-
must_do?: string;
|
|
398
|
-
must_not?: string;
|
|
399
|
-
checks?: string;
|
|
400
|
-
context?: string;
|
|
401
|
-
write?: boolean;
|
|
402
|
-
},
|
|
403
|
-
worktree: string,
|
|
404
|
-
): Promise<string> {
|
|
405
|
-
if (!args.bead_id) return "Error: bead_id required";
|
|
406
|
-
if (!args.outcome) return "Error: outcome required";
|
|
407
|
-
|
|
408
|
-
const split = (s?: string) =>
|
|
409
|
-
s
|
|
410
|
-
?.split(/[,\n]/)
|
|
411
|
-
.map((x) => x.trim())
|
|
412
|
-
.filter(Boolean) || [];
|
|
413
|
-
const bullets = (items: string[]) =>
|
|
414
|
-
items.length ? items.map((i) => `- ${i}`).join("\n") : "- (none)";
|
|
415
|
-
|
|
416
|
-
const packet = [
|
|
417
|
-
"# Delegation Packet",
|
|
418
|
-
"",
|
|
419
|
-
`- TASK: ${args.bead_id}${args.title ? ` - ${args.title}` : ""}`,
|
|
420
|
-
`- EXPECTED OUTCOME: ${args.outcome}`,
|
|
421
|
-
"- MUST DO:",
|
|
422
|
-
bullets(split(args.must_do)),
|
|
423
|
-
"- MUST NOT DO:",
|
|
424
|
-
bullets(split(args.must_not)),
|
|
425
|
-
"- ACCEPTANCE CHECKS:",
|
|
426
|
-
bullets(split(args.checks)),
|
|
427
|
-
"- CONTEXT:",
|
|
428
|
-
args.context || "(none)",
|
|
429
|
-
].join("\n");
|
|
430
|
-
|
|
431
|
-
if (!args.write) return packet;
|
|
432
|
-
|
|
433
|
-
const artifactDir = path.join(worktree, ".beads", "artifacts", args.bead_id);
|
|
434
|
-
const outPath = path.join(artifactDir, "delegation.md");
|
|
435
|
-
|
|
436
|
-
try {
|
|
437
|
-
await fs.mkdir(artifactDir, { recursive: true });
|
|
438
|
-
await fs.appendFile(
|
|
439
|
-
outPath,
|
|
440
|
-
`\n---\nGenerated: ${new Date().toISOString()}\n---\n\n${packet}\n`,
|
|
441
|
-
"utf-8",
|
|
442
|
-
);
|
|
443
|
-
return `✓ Delegation packet written to ${outPath}\n\n${packet}`;
|
|
444
|
-
} catch (error: unknown) {
|
|
445
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
446
|
-
return `Error writing delegation packet: ${message}`;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// ============================================================
|
|
451
|
-
// SYNC OPERATION (from beads-sync.ts)
|
|
452
|
-
// ============================================================
|
|
453
|
-
|
|
454
|
-
const OPENCODE_TODO_DIR = path.join(
|
|
455
|
-
process.env.HOME || "",
|
|
456
|
-
".local",
|
|
457
|
-
"share",
|
|
458
|
-
"opencode",
|
|
459
|
-
"storage",
|
|
460
|
-
"todo",
|
|
461
|
-
);
|
|
462
|
-
|
|
463
|
-
interface BeadTask {
|
|
464
|
-
id: string;
|
|
465
|
-
title: string;
|
|
466
|
-
status: string;
|
|
467
|
-
priority: number;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
async function syncOperation(
|
|
471
|
-
args: { action?: MonitorAction | SyncAction; filter?: SyncFilter },
|
|
472
|
-
worktree: string,
|
|
473
|
-
): Promise<string> {
|
|
474
|
-
if (args.action && args.action !== "push" && args.action !== "pull") {
|
|
475
|
-
return `Invalid sync action: ${args.action}`;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const action: SyncAction = (args.action as SyncAction) || "push";
|
|
479
|
-
|
|
480
|
-
if (action === "push") {
|
|
481
|
-
return pushBeadsToTodos(worktree, args.filter || "open");
|
|
482
|
-
}
|
|
483
|
-
if (action === "pull") {
|
|
484
|
-
return pullTodosToBeads(worktree);
|
|
485
|
-
}
|
|
486
|
-
return `Unknown sync action: ${action}`;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
async function pushBeadsToTodos(
|
|
490
|
-
worktree: string,
|
|
491
|
-
filter: SyncFilter,
|
|
492
|
-
): Promise<string> {
|
|
493
|
-
try {
|
|
494
|
-
const args = ["list", "--json"];
|
|
495
|
-
if (filter !== "all") {
|
|
496
|
-
args.push("--status", filter);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const { stdout } = await execFileAsync("br", args, {
|
|
500
|
-
cwd: worktree,
|
|
501
|
-
timeout: 15000,
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
let tasks: BeadTask[];
|
|
505
|
-
try {
|
|
506
|
-
tasks = JSON.parse(stdout);
|
|
507
|
-
} catch {
|
|
508
|
-
tasks = parseBeadListOutput(stdout);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (tasks.length === 0) {
|
|
512
|
-
return JSON.stringify({
|
|
513
|
-
success: true,
|
|
514
|
-
message: "No tasks to sync",
|
|
515
|
-
synced: 0,
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const todos = tasks.map((t) => ({
|
|
520
|
-
id: t.id,
|
|
521
|
-
content: `[Bead] ${t.title}`,
|
|
522
|
-
status:
|
|
523
|
-
t.status === "closed"
|
|
524
|
-
? "completed"
|
|
525
|
-
: t.status === "in_progress"
|
|
526
|
-
? "in_progress"
|
|
527
|
-
: "pending",
|
|
528
|
-
priority: t.priority <= 1 ? "high" : t.priority <= 2 ? "medium" : "low",
|
|
529
|
-
beadId: t.id,
|
|
530
|
-
}));
|
|
531
|
-
|
|
532
|
-
const sessionId = `ses_${Date.now().toString(36)}_${path.basename(worktree).slice(0, 10)}`;
|
|
533
|
-
const todoPath = path.join(OPENCODE_TODO_DIR, `${sessionId}.json`);
|
|
534
|
-
|
|
535
|
-
await fs.mkdir(OPENCODE_TODO_DIR, { recursive: true });
|
|
536
|
-
await fs.writeFile(todoPath, JSON.stringify(todos, null, 2), "utf-8");
|
|
537
|
-
|
|
538
|
-
return JSON.stringify({
|
|
539
|
-
success: true,
|
|
540
|
-
synced: todos.length,
|
|
541
|
-
session_id: sessionId,
|
|
542
|
-
});
|
|
543
|
-
} catch (error: unknown) {
|
|
544
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
545
|
-
return JSON.stringify({ success: false, error: message });
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
async function pullTodosToBeads(worktree: string): Promise<string> {
|
|
550
|
-
// Simplified: scan todo files and close completed beads
|
|
551
|
-
try {
|
|
552
|
-
const files = await fs.readdir(OPENCODE_TODO_DIR).catch(() => []);
|
|
553
|
-
let updated = 0;
|
|
554
|
-
|
|
555
|
-
for (const file of files) {
|
|
556
|
-
if (!file.endsWith(".json")) continue;
|
|
557
|
-
const content = await fs.readFile(
|
|
558
|
-
path.join(OPENCODE_TODO_DIR, file),
|
|
559
|
-
"utf-8",
|
|
560
|
-
);
|
|
561
|
-
const todos = JSON.parse(content);
|
|
562
|
-
|
|
563
|
-
for (const todo of todos) {
|
|
564
|
-
if (todo.beadId && todo.status === "completed") {
|
|
565
|
-
try {
|
|
566
|
-
await execFileAsync(
|
|
567
|
-
"br",
|
|
568
|
-
["close", todo.beadId, "--reason", "Completed via todo"],
|
|
569
|
-
{
|
|
570
|
-
cwd: worktree,
|
|
571
|
-
timeout: 15000,
|
|
572
|
-
},
|
|
573
|
-
);
|
|
574
|
-
updated++;
|
|
575
|
-
} catch {
|
|
576
|
-
// Already closed
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
return JSON.stringify({ success: true, updated });
|
|
583
|
-
} catch (error: unknown) {
|
|
584
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
585
|
-
return JSON.stringify({ success: false, error: message });
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function parseBeadListOutput(output: string): BeadTask[] {
|
|
590
|
-
const lines = output.trim().split("\n").filter(Boolean);
|
|
591
|
-
const tasks: BeadTask[] = [];
|
|
592
|
-
|
|
593
|
-
for (const line of lines) {
|
|
594
|
-
const match = line.match(/^#?(\S+)\s+\[(\w+)\]\s+(?:\(P(\d)\))?\s*(.+)$/);
|
|
595
|
-
if (match) {
|
|
596
|
-
tasks.push({
|
|
597
|
-
id: match[1],
|
|
598
|
-
status: match[2],
|
|
599
|
-
priority: match[3] ? Number.parseInt(match[3]) : 2,
|
|
600
|
-
title: match[4],
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
return tasks;
|
|
605
|
-
}
|