opencodekit 0.14.1 → 0.14.3
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/.background-tasks.json +96 -0
- package/dist/template/.opencode/.ralph-state.json +12 -0
- package/dist/template/.opencode/AGENTS.md +76 -5
- package/dist/template/.opencode/agent/build.md +45 -8
- package/dist/template/.opencode/agent/looker.md +124 -0
- package/dist/template/.opencode/agent/planner.md +93 -0
- package/dist/template/.opencode/agent/rush.md +36 -6
- package/dist/template/.opencode/agent/scout.md +0 -1
- package/dist/template/.opencode/agent/vision.md +0 -1
- package/dist/template/.opencode/command/implement.md +51 -10
- package/dist/template/.opencode/command/new-feature.md +68 -10
- package/dist/template/.opencode/command/plan.md +59 -10
- package/dist/template/.opencode/command/ralph-loop.md +97 -0
- package/dist/template/.opencode/command/start.md +13 -10
- package/dist/template/.opencode/memory/{project/beads-workflow.md → beads-workflow.md} +53 -0
- package/dist/template/.opencode/memory/project/conventions.md +53 -3
- package/dist/template/.opencode/opencode.json +7 -3
- package/dist/template/.opencode/package.json +2 -1
- package/dist/template/.opencode/plugin/lsp.ts +304 -0
- package/dist/template/.opencode/plugin/ralph-wiggum.ts +182 -0
- package/dist/template/.opencode/tool/background.ts +461 -0
- package/dist/template/.opencode/tool/ralph.ts +203 -0
- package/package.json +1 -1
- /package/dist/template/.opencode/memory/{project/README.md → README.md} +0 -0
- /package/dist/template/.opencode/plugin/{notification.ts → notification.ts.bak} +0 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { tool } from "@opencode-ai/plugin";
|
|
5
|
+
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
6
|
+
|
|
7
|
+
const TASKS_FILE = ".opencode/.background-tasks.json";
|
|
8
|
+
|
|
9
|
+
interface BackgroundTask {
|
|
10
|
+
taskId: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
parentSessionId: string; // Track parent for debugging
|
|
13
|
+
agent: string;
|
|
14
|
+
prompt: string;
|
|
15
|
+
started: number;
|
|
16
|
+
status: "running" | "completed" | "cancelled";
|
|
17
|
+
// Beads integration
|
|
18
|
+
beadId?: string;
|
|
19
|
+
autoCloseBead?: boolean; // Only allowed for safe agents (explore, scout)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface TasksStore {
|
|
23
|
+
tasks: Record<string, BackgroundTask>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function loadTasks(): Promise<TasksStore> {
|
|
27
|
+
try {
|
|
28
|
+
const content = await fs.readFile(TASKS_FILE, "utf-8");
|
|
29
|
+
return JSON.parse(content);
|
|
30
|
+
} catch {
|
|
31
|
+
return { tasks: {} };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function saveTasks(store: TasksStore): Promise<void> {
|
|
36
|
+
await fs.mkdir(path.dirname(TASKS_FILE), { recursive: true });
|
|
37
|
+
await fs.writeFile(TASKS_FILE, JSON.stringify(store, null, 2));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createClient() {
|
|
41
|
+
return createOpencodeClient({ baseUrl: "http://localhost:4096" });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Find the bd binary path dynamically
|
|
46
|
+
*/
|
|
47
|
+
function findBdPath(): string {
|
|
48
|
+
try {
|
|
49
|
+
// Try to find bd in PATH using shell
|
|
50
|
+
const result = execSync("which bd || command -v bd", {
|
|
51
|
+
encoding: "utf-8",
|
|
52
|
+
timeout: 5000,
|
|
53
|
+
shell: "/bin/sh",
|
|
54
|
+
}).trim();
|
|
55
|
+
if (result) return result;
|
|
56
|
+
} catch {
|
|
57
|
+
// Fallback to common locations
|
|
58
|
+
const commonPaths = [
|
|
59
|
+
`${process.env.HOME}/.local/bin/bd`,
|
|
60
|
+
`${process.env.HOME}/.bun/bin/bd`,
|
|
61
|
+
"/usr/local/bin/bd",
|
|
62
|
+
"/opt/homebrew/bin/bd",
|
|
63
|
+
];
|
|
64
|
+
for (const p of commonPaths) {
|
|
65
|
+
try {
|
|
66
|
+
execSync(`test -x "${p}"`, { timeout: 1000 });
|
|
67
|
+
return p;
|
|
68
|
+
} catch {
|
|
69
|
+
// Try next path
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Last resort - assume it's in PATH
|
|
74
|
+
return "bd";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Cache the bd path
|
|
78
|
+
let bdPath: string | null = null;
|
|
79
|
+
function getBdPath(): string {
|
|
80
|
+
if (!bdPath) {
|
|
81
|
+
bdPath = findBdPath();
|
|
82
|
+
}
|
|
83
|
+
return bdPath;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Helper to run beads CLI commands
|
|
88
|
+
*/
|
|
89
|
+
async function runBeadsCommand(
|
|
90
|
+
args: string[],
|
|
91
|
+
): Promise<{ success: boolean; output: string }> {
|
|
92
|
+
try {
|
|
93
|
+
// Quote arguments that contain spaces
|
|
94
|
+
const quotedArgs = args.map((arg) =>
|
|
95
|
+
arg.includes(" ") ? `"${arg}"` : arg,
|
|
96
|
+
);
|
|
97
|
+
// Use dynamically detected bd path with shell for proper PATH resolution
|
|
98
|
+
const output = execSync(`${getBdPath()} ${quotedArgs.join(" ")}`, {
|
|
99
|
+
encoding: "utf-8",
|
|
100
|
+
timeout: 30000,
|
|
101
|
+
shell: "/bin/sh",
|
|
102
|
+
env: { ...process.env },
|
|
103
|
+
});
|
|
104
|
+
return { success: true, output };
|
|
105
|
+
} catch (e) {
|
|
106
|
+
const error = e as { stderr?: string; message?: string };
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
output: error.stderr || error.message || String(e),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Allowed agents for background delegation
|
|
115
|
+
// - Subagents: explore, scout, review, planner, vision, looker (stateless workers)
|
|
116
|
+
// - Primary: rush (autonomous execution)
|
|
117
|
+
// - NOT allowed: build (build is the orchestrator that uses this tool)
|
|
118
|
+
const ALLOWED_AGENTS = [
|
|
119
|
+
"explore",
|
|
120
|
+
"scout",
|
|
121
|
+
"review",
|
|
122
|
+
"planner",
|
|
123
|
+
"vision",
|
|
124
|
+
"looker",
|
|
125
|
+
"rush",
|
|
126
|
+
] as const;
|
|
127
|
+
type AllowedAgent = (typeof ALLOWED_AGENTS)[number];
|
|
128
|
+
|
|
129
|
+
// Agents safe for autoCloseBead (pure research, no side effects)
|
|
130
|
+
// These only return information, don't make changes that need verification
|
|
131
|
+
const SAFE_AUTOCLOSE_AGENTS: readonly string[] = [
|
|
132
|
+
"explore",
|
|
133
|
+
"scout",
|
|
134
|
+
"looker",
|
|
135
|
+
] as const;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Start a background subagent task.
|
|
139
|
+
* Creates a child session that runs independently.
|
|
140
|
+
*/
|
|
141
|
+
export const start = tool({
|
|
142
|
+
description:
|
|
143
|
+
"Start a background subagent task. Returns a task_id to collect results later. Use for parallel research/exploration or executing beads subtasks. NOTE: Cannot delegate to 'build' agent (build is the orchestrator).",
|
|
144
|
+
args: {
|
|
145
|
+
agent: tool.schema
|
|
146
|
+
.string()
|
|
147
|
+
.describe(
|
|
148
|
+
"Agent type: explore, scout, review, planner, vision, looker, rush (NOT build - build is the orchestrator)",
|
|
149
|
+
),
|
|
150
|
+
prompt: tool.schema.string().describe("Task prompt for the agent"),
|
|
151
|
+
title: tool.schema
|
|
152
|
+
.string()
|
|
153
|
+
.optional()
|
|
154
|
+
.describe("Optional task title for identification"),
|
|
155
|
+
beadId: tool.schema
|
|
156
|
+
.string()
|
|
157
|
+
.optional()
|
|
158
|
+
.describe("Bead ID to associate with this task (e.g., bd-abc123)"),
|
|
159
|
+
autoCloseBead: tool.schema
|
|
160
|
+
.boolean()
|
|
161
|
+
.optional()
|
|
162
|
+
.describe(
|
|
163
|
+
"Auto-close bead on completion. Only allowed for safe agents (explore, scout). Blocked for rush/review/planner/vision/looker.",
|
|
164
|
+
),
|
|
165
|
+
},
|
|
166
|
+
execute: async (args, context) => {
|
|
167
|
+
// Validate agent type - build cannot delegate to itself
|
|
168
|
+
if (args.agent === "build") {
|
|
169
|
+
return JSON.stringify({
|
|
170
|
+
error:
|
|
171
|
+
"Cannot delegate to 'build' agent. Build is the orchestrator that uses this tool. Use subagents (explore, scout, review, planner, vision, looker) or rush instead.",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!ALLOWED_AGENTS.includes(args.agent as AllowedAgent)) {
|
|
176
|
+
return JSON.stringify({
|
|
177
|
+
error: `Invalid agent type: ${args.agent}. Allowed: ${ALLOWED_AGENTS.join(", ")}`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Validate autoCloseBead - only allowed for safe agents
|
|
182
|
+
if (args.autoCloseBead && !SAFE_AUTOCLOSE_AGENTS.includes(args.agent)) {
|
|
183
|
+
return JSON.stringify({
|
|
184
|
+
error: `autoCloseBead not allowed for '${args.agent}' agent. Only safe for: ${SAFE_AUTOCLOSE_AGENTS.join(", ")}. Build agent must verify output from ${args.agent} before closing beads.`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const client = createClient();
|
|
189
|
+
const taskId = `bg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
190
|
+
const title = args.title || `bg-${args.agent}-${Date.now()}`;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
// Create child session linked to parent (build agent's session)
|
|
194
|
+
// This enables context inheritance from the main session
|
|
195
|
+
const session = await client.session.create({
|
|
196
|
+
body: {
|
|
197
|
+
title,
|
|
198
|
+
parentID: context.sessionID, // Link to parent session for context inheritance
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (!session.data?.id) {
|
|
203
|
+
return JSON.stringify({ error: "Failed to create session" });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Fire the prompt (this returns immediately, session runs async)
|
|
207
|
+
// Use the agent field and AgentPartInput to properly route to the specified agent
|
|
208
|
+
await client.session.prompt({
|
|
209
|
+
path: { id: session.data.id },
|
|
210
|
+
body: {
|
|
211
|
+
agent: args.agent, // Specify agent type directly in body
|
|
212
|
+
parts: [
|
|
213
|
+
{
|
|
214
|
+
type: "agent" as const,
|
|
215
|
+
name: args.agent, // AgentPartInput triggers agent routing
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
type: "text" as const,
|
|
219
|
+
text: args.prompt,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Persist task info
|
|
226
|
+
const store = await loadTasks();
|
|
227
|
+
store.tasks[taskId] = {
|
|
228
|
+
taskId,
|
|
229
|
+
sessionId: session.data.id,
|
|
230
|
+
parentSessionId: context.sessionID,
|
|
231
|
+
agent: args.agent,
|
|
232
|
+
prompt: args.prompt,
|
|
233
|
+
started: Date.now(),
|
|
234
|
+
status: "running",
|
|
235
|
+
beadId: args.beadId,
|
|
236
|
+
autoCloseBead: args.autoCloseBead,
|
|
237
|
+
};
|
|
238
|
+
await saveTasks(store);
|
|
239
|
+
|
|
240
|
+
// If beadId provided, mark it as in_progress
|
|
241
|
+
if (args.beadId) {
|
|
242
|
+
await runBeadsCommand([
|
|
243
|
+
"update",
|
|
244
|
+
args.beadId,
|
|
245
|
+
"--status",
|
|
246
|
+
"in_progress",
|
|
247
|
+
]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return JSON.stringify({
|
|
251
|
+
taskId,
|
|
252
|
+
sessionId: session.data.id,
|
|
253
|
+
agent: args.agent,
|
|
254
|
+
beadId: args.beadId,
|
|
255
|
+
status: "started",
|
|
256
|
+
message: `Background task started. Use background_output(taskId="${taskId}") to get results.`,
|
|
257
|
+
});
|
|
258
|
+
} catch (e) {
|
|
259
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
260
|
+
return JSON.stringify({
|
|
261
|
+
error: `Failed to start background task: ${error}`,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get output from a background task.
|
|
269
|
+
* Retrieves the last assistant message from the child session.
|
|
270
|
+
*/
|
|
271
|
+
export const output = tool({
|
|
272
|
+
description:
|
|
273
|
+
"Get output from a background task. Returns the agent's response or 'still running' if not complete.",
|
|
274
|
+
args: {
|
|
275
|
+
taskId: tool.schema.string().describe("Task ID from background_start"),
|
|
276
|
+
},
|
|
277
|
+
execute: async (args) => {
|
|
278
|
+
const client = createClient();
|
|
279
|
+
const store = await loadTasks();
|
|
280
|
+
const task = store.tasks[args.taskId];
|
|
281
|
+
|
|
282
|
+
if (!task) {
|
|
283
|
+
return JSON.stringify({
|
|
284
|
+
error: `Task not found: ${args.taskId}`,
|
|
285
|
+
availableTasks: Object.keys(store.tasks),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const messages = await client.session.messages({
|
|
291
|
+
path: { id: task.sessionId },
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (!messages.data?.length) {
|
|
295
|
+
return JSON.stringify({
|
|
296
|
+
taskId: args.taskId,
|
|
297
|
+
status: "running",
|
|
298
|
+
message: "No messages yet - task still initializing",
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Find last assistant message
|
|
303
|
+
const assistantMessages = messages.data.filter(
|
|
304
|
+
(m) => m.info?.role === "assistant",
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (!assistantMessages.length) {
|
|
308
|
+
return JSON.stringify({
|
|
309
|
+
taskId: args.taskId,
|
|
310
|
+
status: "running",
|
|
311
|
+
message: "Task running - no response yet",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const lastMessage = assistantMessages[assistantMessages.length - 1];
|
|
316
|
+
const textParts = lastMessage.parts
|
|
317
|
+
?.filter((p) => p.type === "text")
|
|
318
|
+
.map((p) => p.text)
|
|
319
|
+
.join("\n");
|
|
320
|
+
|
|
321
|
+
// Update status
|
|
322
|
+
task.status = "completed";
|
|
323
|
+
await saveTasks(store);
|
|
324
|
+
|
|
325
|
+
// Build result object
|
|
326
|
+
const result: Record<string, unknown> = {
|
|
327
|
+
taskId: args.taskId,
|
|
328
|
+
agent: task.agent,
|
|
329
|
+
status: "completed",
|
|
330
|
+
output: textParts || "(empty response)",
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Handle bead closing
|
|
334
|
+
if (task.beadId) {
|
|
335
|
+
result.beadId = task.beadId;
|
|
336
|
+
|
|
337
|
+
// Auto-close for safe agents (explore, scout)
|
|
338
|
+
if (task.autoCloseBead && SAFE_AUTOCLOSE_AGENTS.includes(task.agent)) {
|
|
339
|
+
const closeResult = await runBeadsCommand([
|
|
340
|
+
"close",
|
|
341
|
+
task.beadId,
|
|
342
|
+
"--reason",
|
|
343
|
+
`Auto-closed: ${task.agent} task completed (${task.taskId})`,
|
|
344
|
+
]);
|
|
345
|
+
result.beadClosed = closeResult.success;
|
|
346
|
+
if (!closeResult.success) {
|
|
347
|
+
result.beadCloseError = closeResult.output;
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
// For unsafe agents or when autoClose not requested, remind to verify
|
|
351
|
+
result.beadAction = `VERIFY output, then run: bd close ${task.beadId} --reason "..." `;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return JSON.stringify(result);
|
|
356
|
+
} catch (e) {
|
|
357
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
358
|
+
return JSON.stringify({
|
|
359
|
+
taskId: args.taskId,
|
|
360
|
+
error: `Failed to get output: ${error}`,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Cancel background tasks.
|
|
368
|
+
* Aborts running sessions and cleans up task records.
|
|
369
|
+
*/
|
|
370
|
+
export const cancel = tool({
|
|
371
|
+
description:
|
|
372
|
+
"Cancel background tasks. Use all=true to cancel all, or specify taskId.",
|
|
373
|
+
args: {
|
|
374
|
+
all: tool.schema
|
|
375
|
+
.boolean()
|
|
376
|
+
.optional()
|
|
377
|
+
.describe("Cancel all background tasks"),
|
|
378
|
+
taskId: tool.schema
|
|
379
|
+
.string()
|
|
380
|
+
.optional()
|
|
381
|
+
.describe("Specific task ID to cancel"),
|
|
382
|
+
},
|
|
383
|
+
execute: async (args) => {
|
|
384
|
+
const client = createClient();
|
|
385
|
+
const store = await loadTasks();
|
|
386
|
+
const cancelled: string[] = [];
|
|
387
|
+
const errors: string[] = [];
|
|
388
|
+
|
|
389
|
+
const tasksToCancel = args.all
|
|
390
|
+
? Object.values(store.tasks).filter((t) => t.status === "running")
|
|
391
|
+
: args.taskId && store.tasks[args.taskId]
|
|
392
|
+
? [store.tasks[args.taskId]]
|
|
393
|
+
: [];
|
|
394
|
+
|
|
395
|
+
if (!tasksToCancel.length) {
|
|
396
|
+
return JSON.stringify({
|
|
397
|
+
message: args.all
|
|
398
|
+
? "No running tasks to cancel"
|
|
399
|
+
: `Task not found: ${args.taskId}`,
|
|
400
|
+
activeTasks: Object.values(store.tasks)
|
|
401
|
+
.filter((t) => t.status === "running")
|
|
402
|
+
.map((t) => t.taskId),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const task of tasksToCancel) {
|
|
407
|
+
try {
|
|
408
|
+
await client.session.abort({ path: { id: task.sessionId } });
|
|
409
|
+
task.status = "cancelled";
|
|
410
|
+
cancelled.push(task.taskId);
|
|
411
|
+
} catch (e) {
|
|
412
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
413
|
+
errors.push(`${task.taskId}: ${error}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
await saveTasks(store);
|
|
418
|
+
|
|
419
|
+
return JSON.stringify({
|
|
420
|
+
cancelled,
|
|
421
|
+
errors: errors.length ? errors : undefined,
|
|
422
|
+
remaining: Object.values(store.tasks)
|
|
423
|
+
.filter((t) => t.status === "running")
|
|
424
|
+
.map((t) => t.taskId),
|
|
425
|
+
});
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* List all background tasks with their status.
|
|
431
|
+
*/
|
|
432
|
+
export const list = tool({
|
|
433
|
+
description: "List all background tasks with their status.",
|
|
434
|
+
args: {
|
|
435
|
+
status: tool.schema
|
|
436
|
+
.enum(["running", "completed", "cancelled", "all"])
|
|
437
|
+
.optional()
|
|
438
|
+
.default("all")
|
|
439
|
+
.describe("Filter by status"),
|
|
440
|
+
},
|
|
441
|
+
execute: async (args) => {
|
|
442
|
+
const store = await loadTasks();
|
|
443
|
+
const tasks = Object.values(store.tasks);
|
|
444
|
+
|
|
445
|
+
const filtered =
|
|
446
|
+
args.status === "all"
|
|
447
|
+
? tasks
|
|
448
|
+
: tasks.filter((t) => t.status === args.status);
|
|
449
|
+
|
|
450
|
+
return JSON.stringify({
|
|
451
|
+
total: filtered.length,
|
|
452
|
+
tasks: filtered.map((t) => ({
|
|
453
|
+
taskId: t.taskId,
|
|
454
|
+
agent: t.agent,
|
|
455
|
+
status: t.status,
|
|
456
|
+
started: new Date(t.started).toISOString(),
|
|
457
|
+
prompt: t.prompt.slice(0, 100) + (t.prompt.length > 100 ? "..." : ""),
|
|
458
|
+
})),
|
|
459
|
+
});
|
|
460
|
+
},
|
|
461
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ralph Wiggum Tools
|
|
3
|
+
*
|
|
4
|
+
* Standalone tools for the Ralph Wiggum autonomous loop pattern.
|
|
5
|
+
* The plugin (ralph-wiggum.ts) handles event listening, these handle user interaction.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { tool } from "@opencode-ai/plugin";
|
|
11
|
+
|
|
12
|
+
const STATE_FILE = ".opencode/.ralph-state.json";
|
|
13
|
+
|
|
14
|
+
interface RalphState {
|
|
15
|
+
active: boolean;
|
|
16
|
+
sessionID: string | null;
|
|
17
|
+
iteration: number;
|
|
18
|
+
maxIterations: number;
|
|
19
|
+
completionPromise: string;
|
|
20
|
+
task: string;
|
|
21
|
+
prdFile: string | null;
|
|
22
|
+
progressFile: string;
|
|
23
|
+
startedAt: number | null;
|
|
24
|
+
mode: "hitl" | "afk";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_STATE: RalphState = {
|
|
28
|
+
active: false,
|
|
29
|
+
sessionID: null,
|
|
30
|
+
iteration: 0,
|
|
31
|
+
maxIterations: 50,
|
|
32
|
+
completionPromise: "<promise>COMPLETE</promise>",
|
|
33
|
+
task: "",
|
|
34
|
+
prdFile: null,
|
|
35
|
+
progressFile: "progress.txt",
|
|
36
|
+
startedAt: null,
|
|
37
|
+
mode: "hitl",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
async function loadState(): Promise<RalphState> {
|
|
41
|
+
try {
|
|
42
|
+
const content = await fs.readFile(STATE_FILE, "utf-8");
|
|
43
|
+
return JSON.parse(content);
|
|
44
|
+
} catch {
|
|
45
|
+
return { ...DEFAULT_STATE };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function saveState(state: RalphState): Promise<void> {
|
|
50
|
+
await fs.mkdir(path.dirname(STATE_FILE), { recursive: true });
|
|
51
|
+
await fs.writeFile(STATE_FILE, JSON.stringify(state, null, 2));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Start Ralph Wiggum autonomous loop
|
|
56
|
+
*/
|
|
57
|
+
export const ralph_start = tool({
|
|
58
|
+
description:
|
|
59
|
+
"Start Ralph Wiggum autonomous loop. Agent will work on tasks until completion or max iterations.",
|
|
60
|
+
args: {
|
|
61
|
+
task: tool.schema
|
|
62
|
+
.string()
|
|
63
|
+
.describe(
|
|
64
|
+
"Task description or goal (e.g., 'Migrate all Jest tests to Vitest')",
|
|
65
|
+
),
|
|
66
|
+
prdFile: tool.schema
|
|
67
|
+
.string()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe("Path to PRD/task list file (e.g., 'PRD.md')"),
|
|
70
|
+
progressFile: tool.schema
|
|
71
|
+
.string()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe("Path to progress tracking file (default: progress.txt)"),
|
|
74
|
+
completionPromise: tool.schema
|
|
75
|
+
.string()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe(
|
|
78
|
+
"Text to output when done (default: <promise>COMPLETE</promise>)",
|
|
79
|
+
),
|
|
80
|
+
maxIterations: tool.schema
|
|
81
|
+
.number()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("Maximum iterations before stopping (default: 50)"),
|
|
84
|
+
mode: tool.schema
|
|
85
|
+
.enum(["hitl", "afk"])
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("Mode: hitl (human-in-the-loop) or afk (away-from-keyboard)"),
|
|
88
|
+
},
|
|
89
|
+
execute: async (args, context) => {
|
|
90
|
+
const state: RalphState = {
|
|
91
|
+
active: true,
|
|
92
|
+
sessionID: context.sessionID,
|
|
93
|
+
iteration: 0,
|
|
94
|
+
maxIterations: args.maxIterations || 50,
|
|
95
|
+
completionPromise:
|
|
96
|
+
args.completionPromise || "<promise>COMPLETE</promise>",
|
|
97
|
+
task: args.task,
|
|
98
|
+
prdFile: args.prdFile || null,
|
|
99
|
+
progressFile: args.progressFile || "progress.txt",
|
|
100
|
+
startedAt: Date.now(),
|
|
101
|
+
mode: args.mode || "hitl",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
await saveState(state);
|
|
105
|
+
|
|
106
|
+
const modeDesc =
|
|
107
|
+
state.mode === "hitl"
|
|
108
|
+
? "Human-in-the-loop (watch and intervene)"
|
|
109
|
+
: "Away-from-keyboard (autonomous)";
|
|
110
|
+
|
|
111
|
+
return `
|
|
112
|
+
## Ralph Loop Active
|
|
113
|
+
|
|
114
|
+
**Task:** ${state.task}
|
|
115
|
+
**Mode:** ${modeDesc}
|
|
116
|
+
**Max Iterations:** ${state.maxIterations}
|
|
117
|
+
**Completion Signal:** ${state.completionPromise}
|
|
118
|
+
**PRD File:** ${state.prdFile || "(none - using task description)"}
|
|
119
|
+
**Progress File:** ${state.progressFile}
|
|
120
|
+
|
|
121
|
+
### Next Steps
|
|
122
|
+
|
|
123
|
+
1. Work on the task described above
|
|
124
|
+
2. After each feature, update ${state.progressFile}
|
|
125
|
+
3. Run feedback loops (typecheck, test, lint)
|
|
126
|
+
4. Commit changes
|
|
127
|
+
5. When ALL tasks complete, output: ${state.completionPromise}
|
|
128
|
+
|
|
129
|
+
The loop will continue automatically after each completion until:
|
|
130
|
+
- You output the completion promise, OR
|
|
131
|
+
- Max iterations (${state.maxIterations}) reached
|
|
132
|
+
|
|
133
|
+
**Remember:** ONE feature per iteration. Small steps. Quality over speed.
|
|
134
|
+
`.trim();
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Stop Ralph Wiggum loop
|
|
140
|
+
*/
|
|
141
|
+
export const ralph_stop = tool({
|
|
142
|
+
description: "Stop the Ralph Wiggum loop gracefully",
|
|
143
|
+
args: {
|
|
144
|
+
reason: tool.schema.string().optional().describe("Reason for stopping"),
|
|
145
|
+
},
|
|
146
|
+
execute: async (args) => {
|
|
147
|
+
const state = await loadState();
|
|
148
|
+
|
|
149
|
+
if (!state.active) {
|
|
150
|
+
return "No Ralph loop is currently running.";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const duration = state.startedAt
|
|
154
|
+
? Math.round((Date.now() - state.startedAt) / 1000 / 60)
|
|
155
|
+
: 0;
|
|
156
|
+
|
|
157
|
+
const summary = `
|
|
158
|
+
## Ralph Loop Stopped
|
|
159
|
+
|
|
160
|
+
**Iterations Completed:** ${state.iteration}
|
|
161
|
+
**Duration:** ${duration} minutes
|
|
162
|
+
**Reason:** ${args.reason || "Manual stop requested"}
|
|
163
|
+
**Task:** ${state.task}
|
|
164
|
+
|
|
165
|
+
Progress has been saved to ${state.progressFile}.
|
|
166
|
+
`.trim();
|
|
167
|
+
|
|
168
|
+
// Reset state
|
|
169
|
+
await saveState(DEFAULT_STATE);
|
|
170
|
+
|
|
171
|
+
return summary;
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get Ralph Wiggum status
|
|
177
|
+
*/
|
|
178
|
+
export const ralph_status = tool({
|
|
179
|
+
description: "Get current Ralph Wiggum loop status",
|
|
180
|
+
args: {},
|
|
181
|
+
execute: async () => {
|
|
182
|
+
const state = await loadState();
|
|
183
|
+
|
|
184
|
+
if (!state.active) {
|
|
185
|
+
return "No Ralph loop is currently active.";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const duration = state.startedAt
|
|
189
|
+
? Math.round((Date.now() - state.startedAt) / 1000 / 60)
|
|
190
|
+
: 0;
|
|
191
|
+
|
|
192
|
+
return `
|
|
193
|
+
## Ralph Loop Active
|
|
194
|
+
|
|
195
|
+
**Task:** ${state.task}
|
|
196
|
+
**Iteration:** ${state.iteration}/${state.maxIterations}
|
|
197
|
+
**Duration:** ${duration} minutes
|
|
198
|
+
**Mode:** ${state.mode}
|
|
199
|
+
**Completion Signal:** ${state.completionPromise}
|
|
200
|
+
**Progress File:** ${state.progressFile}
|
|
201
|
+
`.trim();
|
|
202
|
+
},
|
|
203
|
+
});
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|