opencodekit 0.14.3 → 0.14.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 +7 -7
- package/dist/template/.opencode/.background-tasks.json +18 -0
- package/dist/template/.opencode/command/analyze-project.md +187 -358
- package/dist/template/.opencode/command/implement.md +29 -30
- package/dist/template/.opencode/command/new-feature.md +10 -10
- package/dist/template/.opencode/command/plan.md +13 -15
- package/dist/template/.opencode/command/ralph.md +41 -0
- package/dist/template/.opencode/command/start.md +11 -12
- package/dist/template/.opencode/opencode.json +5 -1
- package/dist/template/.opencode/package.json +1 -1
- package/dist/template/.opencode/plugin/compaction.ts +25 -2
- package/dist/template/.opencode/skill/ralph/SKILL.md +300 -0
- package/dist/template/.opencode/tool/background.ts +147 -99
- package/package.json +7 -7
- package/dist/template/.opencode/command/ralph-loop.md +0 -97
- package/dist/template/.opencode/plugin/handoff.ts +0 -37
- package/dist/template/.opencode/plugin/ralph-wiggum.ts +0 -182
- package/dist/template/.opencode/tool/ralph.ts +0 -203
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ralph Wiggum Plugin for OpenCode
|
|
3
|
-
*
|
|
4
|
-
* Handles the session.idle event to continue the Ralph loop.
|
|
5
|
-
* Tools are defined separately in .opencode/tool/ralph.ts
|
|
6
|
-
*
|
|
7
|
-
* Based on: https://ghuntley.com/ralph/
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import fs from "node:fs/promises";
|
|
11
|
-
import type { Plugin } from "@opencode-ai/plugin";
|
|
12
|
-
|
|
13
|
-
const STATE_FILE = ".opencode/.ralph-state.json";
|
|
14
|
-
const IDLE_DEBOUNCE_MS = 2000;
|
|
15
|
-
let lastIdleTime = 0;
|
|
16
|
-
|
|
17
|
-
interface RalphState {
|
|
18
|
-
active: boolean;
|
|
19
|
-
sessionID: string | null;
|
|
20
|
-
iteration: number;
|
|
21
|
-
maxIterations: number;
|
|
22
|
-
completionPromise: string;
|
|
23
|
-
task: string;
|
|
24
|
-
prdFile: string | null;
|
|
25
|
-
progressFile: string;
|
|
26
|
-
startedAt: number | null;
|
|
27
|
-
mode: "hitl" | "afk";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function loadState(): Promise<RalphState | null> {
|
|
31
|
-
try {
|
|
32
|
-
const content = await fs.readFile(STATE_FILE, "utf-8");
|
|
33
|
-
return JSON.parse(content);
|
|
34
|
-
} catch {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function saveState(state: RalphState): Promise<void> {
|
|
40
|
-
await fs.writeFile(STATE_FILE, JSON.stringify(state, null, 2));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function resetState(): Promise<void> {
|
|
44
|
-
try {
|
|
45
|
-
await fs.unlink(STATE_FILE);
|
|
46
|
-
} catch {
|
|
47
|
-
// File doesn't exist, that's fine
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export const RalphWiggum: Plugin = async ({ client }) => {
|
|
52
|
-
const log = async (
|
|
53
|
-
message: string,
|
|
54
|
-
level: "info" | "warn" | "error" = "info",
|
|
55
|
-
) => {
|
|
56
|
-
await client.app
|
|
57
|
-
.log({
|
|
58
|
-
body: { service: "ralph-wiggum", level, message },
|
|
59
|
-
})
|
|
60
|
-
.catch(() => {});
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const showToast = async (
|
|
64
|
-
title: string,
|
|
65
|
-
message: string,
|
|
66
|
-
variant: "info" | "success" | "warning" | "error" = "info",
|
|
67
|
-
) => {
|
|
68
|
-
await client.tui
|
|
69
|
-
.showToast({
|
|
70
|
-
body: {
|
|
71
|
-
title: `Ralph: ${title}`,
|
|
72
|
-
message,
|
|
73
|
-
variant,
|
|
74
|
-
duration: variant === "error" ? 8000 : 5000,
|
|
75
|
-
},
|
|
76
|
-
})
|
|
77
|
-
.catch(() => {});
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const buildContinuationPrompt = (state: RalphState): string => {
|
|
81
|
-
const prdRef = state.prdFile ? `@${state.prdFile} ` : "";
|
|
82
|
-
const progressRef = `@${state.progressFile}`;
|
|
83
|
-
|
|
84
|
-
return `
|
|
85
|
-
${prdRef}${progressRef}
|
|
86
|
-
|
|
87
|
-
## Ralph Wiggum Loop - Iteration ${state.iteration}/${state.maxIterations}
|
|
88
|
-
|
|
89
|
-
You are in an autonomous loop. Continue working on the task.
|
|
90
|
-
|
|
91
|
-
**Task:** ${state.task}
|
|
92
|
-
|
|
93
|
-
**Instructions:**
|
|
94
|
-
1. Review the PRD/task list and progress file
|
|
95
|
-
2. Choose the highest-priority INCOMPLETE task
|
|
96
|
-
3. Implement ONE feature/change only
|
|
97
|
-
4. Run feedback loops: typecheck, test, lint
|
|
98
|
-
5. Commit if all pass
|
|
99
|
-
6. Update ${state.progressFile}
|
|
100
|
-
7. If ALL tasks complete, output: ${state.completionPromise}
|
|
101
|
-
|
|
102
|
-
**Constraints:** ONE feature per iteration. Quality over speed.
|
|
103
|
-
`.trim();
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const handleSessionIdle = async (sessionID: string): Promise<void> => {
|
|
107
|
-
const now = Date.now();
|
|
108
|
-
if (now - lastIdleTime < IDLE_DEBOUNCE_MS) return;
|
|
109
|
-
lastIdleTime = now;
|
|
110
|
-
|
|
111
|
-
const state = await loadState();
|
|
112
|
-
if (!state?.active || state.sessionID !== sessionID) return;
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
const messagesResponse = await client.session.messages({
|
|
116
|
-
path: { id: sessionID },
|
|
117
|
-
});
|
|
118
|
-
const messages = messagesResponse.data || [];
|
|
119
|
-
const lastMessage = messages[messages.length - 1];
|
|
120
|
-
|
|
121
|
-
const lastText =
|
|
122
|
-
lastMessage?.parts
|
|
123
|
-
?.filter((p) => p.type === "text")
|
|
124
|
-
.map((p) => ("text" in p ? (p.text as string) : ""))
|
|
125
|
-
.join("") || "";
|
|
126
|
-
|
|
127
|
-
if (lastText.includes(state.completionPromise)) {
|
|
128
|
-
const duration = state.startedAt
|
|
129
|
-
? Math.round((Date.now() - state.startedAt) / 1000 / 60)
|
|
130
|
-
: 0;
|
|
131
|
-
await showToast(
|
|
132
|
-
"Complete!",
|
|
133
|
-
`Finished in ${state.iteration} iterations (${duration} min)`,
|
|
134
|
-
"success",
|
|
135
|
-
);
|
|
136
|
-
await log(`Loop completed in ${state.iteration} iterations`);
|
|
137
|
-
await resetState();
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
state.iteration++;
|
|
142
|
-
if (state.iteration >= state.maxIterations) {
|
|
143
|
-
await showToast(
|
|
144
|
-
"Stopped",
|
|
145
|
-
`Max iterations (${state.maxIterations}) reached`,
|
|
146
|
-
"warning",
|
|
147
|
-
);
|
|
148
|
-
await log(`Max iterations reached: ${state.maxIterations}`, "warn");
|
|
149
|
-
await resetState();
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
await saveState(state);
|
|
154
|
-
|
|
155
|
-
await client.session.prompt({
|
|
156
|
-
path: { id: sessionID },
|
|
157
|
-
body: {
|
|
158
|
-
parts: [{ type: "text", text: buildContinuationPrompt(state) }],
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
await log(`Iteration ${state.iteration}/${state.maxIterations}`);
|
|
163
|
-
} catch (error) {
|
|
164
|
-
await log(`Error in Ralph loop: ${error}`, "error");
|
|
165
|
-
await resetState();
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
event: async ({ event }) => {
|
|
171
|
-
if (event.type === "session.idle") {
|
|
172
|
-
const sessionID = (event as { properties?: { sessionID?: string } })
|
|
173
|
-
.properties?.sessionID;
|
|
174
|
-
if (sessionID) {
|
|
175
|
-
await handleSessionIdle(sessionID);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
};
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
export default RalphWiggum;
|
|
@@ -1,203 +0,0 @@
|
|
|
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
|
-
});
|