opencodekit 0.14.0 → 0.14.2

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.
Files changed (54) hide show
  1. package/dist/index.js +53 -18
  2. package/dist/template/.opencode/.background-tasks.json +96 -0
  3. package/dist/template/.opencode/.ralph-state.json +12 -0
  4. package/dist/template/.opencode/AGENTS.md +112 -6
  5. package/dist/template/.opencode/agent/build.md +60 -8
  6. package/dist/template/.opencode/agent/explore.md +1 -0
  7. package/dist/template/.opencode/agent/looker.md +124 -0
  8. package/dist/template/.opencode/agent/planner.md +40 -1
  9. package/dist/template/.opencode/agent/review.md +1 -0
  10. package/dist/template/.opencode/agent/rush.md +53 -6
  11. package/dist/template/.opencode/agent/scout.md +1 -1
  12. package/dist/template/.opencode/agent/vision.md +0 -1
  13. package/dist/template/.opencode/command/brainstorm.md +58 -3
  14. package/dist/template/.opencode/command/finish.md +18 -8
  15. package/dist/template/.opencode/command/fix.md +24 -15
  16. package/dist/template/.opencode/command/implement.md +95 -29
  17. package/dist/template/.opencode/command/import-plan.md +30 -8
  18. package/dist/template/.opencode/command/new-feature.md +105 -14
  19. package/dist/template/.opencode/command/plan.md +78 -11
  20. package/dist/template/.opencode/command/pr.md +25 -15
  21. package/dist/template/.opencode/command/ralph-loop.md +97 -0
  22. package/dist/template/.opencode/command/revert-feature.md +15 -3
  23. package/dist/template/.opencode/command/skill-optimize.md +71 -7
  24. package/dist/template/.opencode/command/start.md +63 -15
  25. package/dist/template/.opencode/dcp.jsonc +11 -7
  26. package/dist/template/.opencode/memory/{project/beads-workflow.md → beads-workflow.md} +53 -0
  27. package/dist/template/.opencode/memory/observations/2026-01-09-pattern-ampcode-mcp-json-includetools-pattern.md +42 -0
  28. package/dist/template/.opencode/memory/project/conventions.md +53 -3
  29. package/dist/template/.opencode/memory/project/gotchas.md +52 -5
  30. package/dist/template/.opencode/memory/vector_db/memories.lance/_transactions/{0-8d00d272-cb80-463b-9774-7120a1c994e7.txn → 0-0d25ba80-ba3b-4209-9046-b45d6093b4da.txn} +0 -0
  31. package/dist/template/.opencode/memory/vector_db/memories.lance/_versions/1.manifest +0 -0
  32. package/dist/template/.opencode/memory/vector_db/memories.lance/data/{001010101000000101110001f998d04b63936ff83f9a34152d.lance → 1111100101010101011010004a9ef34df6b29f36a9a53a2892.lance} +0 -0
  33. package/dist/template/.opencode/opencode.json +529 -587
  34. package/dist/template/.opencode/package.json +2 -1
  35. package/dist/template/.opencode/plugin/lsp.ts +299 -0
  36. package/dist/template/.opencode/plugin/memory.ts +77 -1
  37. package/dist/template/.opencode/plugin/package.json +1 -1
  38. package/dist/template/.opencode/plugin/ralph-wiggum.ts +182 -0
  39. package/dist/template/.opencode/plugin/skill-mcp.ts +155 -36
  40. package/dist/template/.opencode/skill/chrome-devtools/SKILL.md +43 -65
  41. package/dist/template/.opencode/skill/chrome-devtools/mcp.json +19 -0
  42. package/dist/template/.opencode/skill/executing-plans/SKILL.md +32 -2
  43. package/dist/template/.opencode/skill/finishing-a-development-branch/SKILL.md +42 -17
  44. package/dist/template/.opencode/skill/playwright/SKILL.md +58 -133
  45. package/dist/template/.opencode/skill/playwright/mcp.json +16 -0
  46. package/dist/template/.opencode/tool/background.ts +461 -0
  47. package/dist/template/.opencode/tool/memory-search.ts +2 -2
  48. package/dist/template/.opencode/tool/ralph.ts +203 -0
  49. package/package.json +4 -16
  50. package/dist/template/.opencode/memory/vector_db/memories.lance/_transactions/1-a3bea825-dad3-47dd-a6d6-ff41b76ff7b0.txn +0 -0
  51. package/dist/template/.opencode/memory/vector_db/memories.lance/_versions/2.manifest +0 -0
  52. package/dist/template/.opencode/memory/vector_db/memories.lance/data/010000101010000000010010701b3840d38c2b5f275da99978.lance +0 -0
  53. /package/dist/template/.opencode/memory/{project/README.md → README.md} +0 -0
  54. /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
+ continue;
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
+ });
@@ -246,7 +246,7 @@ function formatKeywordResults(
246
246
  ) {
247
247
  const lineNum = result.matches[0]?.line;
248
248
  if (lineNum) {
249
- output += `\n🔍 **LSP Nudge:**\n`;
249
+ output += "\n🔍 **LSP Nudge:**\n";
250
250
  output += ` \`lsp_lsp_goto_definition({ filePath: "${result.file}", line: ${lineNum}, character: 1 })\`\n`;
251
251
  }
252
252
  }
@@ -297,7 +297,7 @@ function formatSemanticResults(
297
297
  result.file.endsWith(".go") ||
298
298
  result.file.endsWith(".rs")
299
299
  ) {
300
- output += `\n🔍 **LSP Nudge:**\n`;
300
+ output += "\n🔍 **LSP Nudge:**\n";
301
301
  output += ` Get symbols: \`lsp_lsp_document_symbols({ filePath: "${result.file}" })\`\n`;
302
302
  output += ` Find references: \`lsp_lsp_find_references({ filePath: "${result.file}", line: 1, character: 1 })\`\n`;
303
303
  }
@@ -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
+ });