opencodekit 0.14.3 → 0.14.4
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/command/ralph.md +41 -0
- package/dist/template/.opencode/opencode.json +5 -1
- package/dist/template/.opencode/plugin/compaction.ts +25 -2
- package/dist/template/.opencode/skill/ralph/SKILL.md +300 -0
- package/package.json +1 -1
- 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
package/dist/index.js
CHANGED
|
@@ -750,7 +750,7 @@ var cac = (name = "") => new CAC(name);
|
|
|
750
750
|
// package.json
|
|
751
751
|
var package_default = {
|
|
752
752
|
name: "opencodekit",
|
|
753
|
-
version: "0.14.
|
|
753
|
+
version: "0.14.4",
|
|
754
754
|
description: "CLI tool for bootstrapping and managing OpenCodeKit projects",
|
|
755
755
|
type: "module",
|
|
756
756
|
repository: {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Start Ralph Wiggum autonomous loop for task completion
|
|
3
|
+
argument-hint: "<task> [--prd <file>]"
|
|
4
|
+
agent: build
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Ralph Wiggum Loop
|
|
8
|
+
|
|
9
|
+
First, load the ralph skill for complete instructions:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
skill({ name: "ralph" });
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Task
|
|
16
|
+
|
|
17
|
+
$ARGUMENTS
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
1. **Load the skill** (above) to get full workflow instructions
|
|
22
|
+
2. **Detect environment**: Check lock files to determine package manager
|
|
23
|
+
3. **Create progress.txt**: Track completed tasks and notes
|
|
24
|
+
4. **Start iterating**: Follow the loop pattern from the skill
|
|
25
|
+
|
|
26
|
+
## The Loop (Summary)
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
Read PRD → Pick task → Implement ONE feature → Validate → Commit → Update progress → Repeat
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Exit when ALL tasks complete by outputting:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
<promise>COMPLETE</promise>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## See Also
|
|
39
|
+
|
|
40
|
+
- Full instructions: `skill({ name: "ralph" })`
|
|
41
|
+
- Skill location: `.opencode/skill/ralph/SKILL.md`
|
|
@@ -77,7 +77,11 @@
|
|
|
77
77
|
"extensions": [".html", ".css", ".scss", ".sass", ".md", ".yaml", ".yml"]
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
|
-
"instructions": [
|
|
80
|
+
"instructions": [
|
|
81
|
+
".opencode/memory/user.md",
|
|
82
|
+
".opencode/memory/project/*.md",
|
|
83
|
+
".opencode/memory/observations/*.md"
|
|
84
|
+
],
|
|
81
85
|
"keybinds": {
|
|
82
86
|
"command_list": ";",
|
|
83
87
|
"leader": "`",
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* 1. Load session-context.md (CONTINUITY.md pattern)
|
|
6
6
|
* 2. Load project memory files
|
|
7
7
|
* 3. Inject beads in-progress state
|
|
8
|
-
* 4.
|
|
8
|
+
* 4. Load most recent handoff file for session resumption
|
|
9
|
+
* 5. Append workflow-specific compaction rules
|
|
9
10
|
*
|
|
10
11
|
* Session context format (agent-maintained via memory-update):
|
|
11
12
|
* - Goal: What we're trying to achieve + success criteria
|
|
@@ -20,6 +21,7 @@ import type { Plugin } from "@opencode-ai/plugin";
|
|
|
20
21
|
|
|
21
22
|
export const CompactionPlugin: Plugin = async ({ $, directory }) => {
|
|
22
23
|
const MEMORY_DIR = `${directory}/.opencode/memory`;
|
|
24
|
+
const HANDOFF_DIR = `${MEMORY_DIR}/handoffs`;
|
|
23
25
|
|
|
24
26
|
return {
|
|
25
27
|
"experimental.session.compacting": async (input, output) => {
|
|
@@ -69,8 +71,29 @@ export const CompactionPlugin: Plugin = async ({ $, directory }) => {
|
|
|
69
71
|
// Beads not available, skip
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
// 4. Load most recent handoff file (session continuity)
|
|
75
|
+
let handoffContext = "";
|
|
76
|
+
try {
|
|
77
|
+
const result =
|
|
78
|
+
await $`ls -t ${HANDOFF_DIR}/*.md 2>/dev/null | head -1`.quiet();
|
|
79
|
+
if (result.stdout) {
|
|
80
|
+
const handoffPath = result.stdout.toString().trim();
|
|
81
|
+
if (handoffPath) {
|
|
82
|
+
const handoffContent = await $`cat ${handoffPath}`.text();
|
|
83
|
+
handoffContext = `\n## Previous Session Handoff\n\n${handoffContent}\n\n**IMPORTANT**: Resume work from where previous session left off.`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// No handoff files, skip
|
|
88
|
+
}
|
|
89
|
+
|
|
72
90
|
// Inject all context - session context FIRST (most important)
|
|
73
|
-
const allContext = [
|
|
91
|
+
const allContext = [
|
|
92
|
+
sessionContext,
|
|
93
|
+
beadsContext,
|
|
94
|
+
handoffContext,
|
|
95
|
+
memoryContext,
|
|
96
|
+
]
|
|
74
97
|
.filter(Boolean)
|
|
75
98
|
.join("\n");
|
|
76
99
|
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ralph
|
|
3
|
+
description: Use when running autonomous agent loops for extended task completion without human intervention - handles PRD-driven development, migration tasks, or batch operations
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: opencode
|
|
6
|
+
metadata:
|
|
7
|
+
source: https://ghuntley.com/ralph/
|
|
8
|
+
category: automation
|
|
9
|
+
updated: 2026-01-10
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Ralph Wiggum - Autonomous Agent Loop
|
|
13
|
+
|
|
14
|
+
A workflow pattern for extended autonomous work sessions where the agent iterates through tasks until completion.
|
|
15
|
+
|
|
16
|
+
## When to Use
|
|
17
|
+
|
|
18
|
+
- **PRD-driven development**: Work through a product requirements document task by task
|
|
19
|
+
- **Migration tasks**: "Migrate all Jest tests to Vitest" - agent works until done
|
|
20
|
+
- **Batch operations**: Process multiple files/components with the same pattern
|
|
21
|
+
- **Away-from-keyboard work**: Let the agent work autonomously while you're away
|
|
22
|
+
|
|
23
|
+
## When NOT to Use
|
|
24
|
+
|
|
25
|
+
- Simple single-step tasks (just do them directly)
|
|
26
|
+
- Tasks requiring frequent human decisions
|
|
27
|
+
- Exploratory work where the goal isn't clear
|
|
28
|
+
- Critical production changes (requires human oversight)
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
Invoke the workflow with:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
/ralph Migrate all Jest tests to Vitest --prd PRD.md
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or manually follow the loop pattern below.
|
|
39
|
+
|
|
40
|
+
## The Loop Pattern
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
┌─────────────────────────────────────────────────┐
|
|
44
|
+
│ 1. Read PRD/task list + progress.txt │
|
|
45
|
+
│ 2. Pick highest-priority incomplete task │
|
|
46
|
+
│ 3. Implement ONE feature only │
|
|
47
|
+
│ 4. Run feedback: test → typecheck → lint │
|
|
48
|
+
│ 5. If pass → commit + update progress.txt │
|
|
49
|
+
│ 6. If all done → output <promise>COMPLETE │
|
|
50
|
+
│ 7. Otherwise → repeat from step 1 │
|
|
51
|
+
└─────────────────────────────────────────────────┘
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Using LSP Tools (Experimental)
|
|
55
|
+
|
|
56
|
+
OpenCode provides LSP tools for code intelligence.
|
|
57
|
+
|
|
58
|
+
### Available Operations
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// Understand file structure before editing
|
|
62
|
+
lsp({
|
|
63
|
+
operation: "documentSymbol",
|
|
64
|
+
filePath: "src/auth.ts",
|
|
65
|
+
line: 1,
|
|
66
|
+
character: 1,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Find where a symbol is defined
|
|
70
|
+
lsp({
|
|
71
|
+
operation: "goToDefinition",
|
|
72
|
+
filePath: "src/auth.ts",
|
|
73
|
+
line: 42,
|
|
74
|
+
character: 10,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Find all usages of a symbol (impact analysis)
|
|
78
|
+
lsp({
|
|
79
|
+
operation: "findReferences",
|
|
80
|
+
filePath: "src/auth.ts",
|
|
81
|
+
line: 42,
|
|
82
|
+
character: 10,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Get type info and documentation
|
|
86
|
+
lsp({ operation: "hover", filePath: "src/auth.ts", line: 42, character: 10 });
|
|
87
|
+
|
|
88
|
+
// Find implementations of interface/abstract
|
|
89
|
+
lsp({
|
|
90
|
+
operation: "goToImplementation",
|
|
91
|
+
filePath: "src/types.ts",
|
|
92
|
+
line: 15,
|
|
93
|
+
character: 10,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Search symbols across entire workspace
|
|
97
|
+
lsp({
|
|
98
|
+
operation: "workspaceSymbol",
|
|
99
|
+
filePath: "src/index.ts",
|
|
100
|
+
line: 1,
|
|
101
|
+
character: 1,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Call hierarchy analysis
|
|
105
|
+
lsp({
|
|
106
|
+
operation: "prepareCallHierarchy",
|
|
107
|
+
filePath: "src/api.ts",
|
|
108
|
+
line: 20,
|
|
109
|
+
character: 5,
|
|
110
|
+
});
|
|
111
|
+
lsp({
|
|
112
|
+
operation: "incomingCalls",
|
|
113
|
+
filePath: "src/api.ts",
|
|
114
|
+
line: 20,
|
|
115
|
+
character: 5,
|
|
116
|
+
});
|
|
117
|
+
lsp({
|
|
118
|
+
operation: "outgoingCalls",
|
|
119
|
+
filePath: "src/api.ts",
|
|
120
|
+
line: 20,
|
|
121
|
+
character: 5,
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### LSP-First Workflow
|
|
126
|
+
|
|
127
|
+
Before editing ANY file in the loop:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
1. Read the file
|
|
131
|
+
2. Use documentSymbol to understand structure
|
|
132
|
+
3. Use findReferences to check impact
|
|
133
|
+
4. Use hover for type info
|
|
134
|
+
5. THEN make edits
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This prevents breaking changes and ensures you understand the code before modifying it.
|
|
138
|
+
|
|
139
|
+
## Smart Language Detection
|
|
140
|
+
|
|
141
|
+
OpenCode auto-detects languages via LSP based on file extensions. Use this to determine the project type:
|
|
142
|
+
|
|
143
|
+
### Primary Detection (File Extensions)
|
|
144
|
+
|
|
145
|
+
| Language | Extensions | LSP Server |
|
|
146
|
+
| ---------- | ----------------------------- | ------------- |
|
|
147
|
+
| TypeScript | `.ts`, `.tsx`, `.mts`, `.cts` | typescript |
|
|
148
|
+
| JavaScript | `.js`, `.jsx`, `.mjs`, `.cjs` | typescript |
|
|
149
|
+
| Python | `.py`, `.pyi` | pyright |
|
|
150
|
+
| Rust | `.rs` | rust-analyzer |
|
|
151
|
+
| Go | `.go` | gopls |
|
|
152
|
+
| C/C++ | `.c`, `.cpp`, `.h`, `.hpp` | clangd |
|
|
153
|
+
| Java | `.java` | jdtls |
|
|
154
|
+
| C# | `.cs` | csharp |
|
|
155
|
+
| Ruby | `.rb`, `.rake` | ruby-lsp |
|
|
156
|
+
| PHP | `.php` | intelephense |
|
|
157
|
+
| Swift | `.swift` | sourcekit-lsp |
|
|
158
|
+
| Kotlin | `.kt`, `.kts` | kotlin-ls |
|
|
159
|
+
| Elixir | `.ex`, `.exs` | elixir-ls |
|
|
160
|
+
| Dart | `.dart` | dart |
|
|
161
|
+
| Zig | `.zig` | zls |
|
|
162
|
+
| Gleam | `.gleam` | gleam |
|
|
163
|
+
| Lua | `.lua` | lua-ls |
|
|
164
|
+
| Clojure | `.clj`, `.cljs`, `.cljc` | clojure-lsp |
|
|
165
|
+
| OCaml | `.ml`, `.mli` | ocaml-lsp |
|
|
166
|
+
| Svelte | `.svelte` | svelte |
|
|
167
|
+
| Vue | `.vue` | vue |
|
|
168
|
+
| Astro | `.astro` | astro |
|
|
169
|
+
|
|
170
|
+
### Secondary Detection (Lock Files → Package Manager)
|
|
171
|
+
|
|
172
|
+
| Lock File | Package Manager | Runtime |
|
|
173
|
+
| ------------------- | --------------- | ------- |
|
|
174
|
+
| `bun.lockb` | Bun | Bun |
|
|
175
|
+
| `yarn.lock` | Yarn | Node |
|
|
176
|
+
| `pnpm-lock.yaml` | pnpm | Node |
|
|
177
|
+
| `package-lock.json` | npm | Node |
|
|
178
|
+
| `deno.json` | Deno | Deno |
|
|
179
|
+
| `Cargo.toml` | Cargo | Rust |
|
|
180
|
+
| `go.mod` | Go Modules | Go |
|
|
181
|
+
| `pyproject.toml` | Poetry/pip | Python |
|
|
182
|
+
| `Gemfile.lock` | Bundler | Ruby |
|
|
183
|
+
| `composer.lock` | Composer | PHP |
|
|
184
|
+
| `Package.swift` | SwiftPM | Swift |
|
|
185
|
+
| `mix.lock` | Mix | Elixir |
|
|
186
|
+
| `pubspec.lock` | Pub | Dart |
|
|
187
|
+
|
|
188
|
+
### Validation Commands by Language
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# TypeScript/JavaScript (Bun)
|
|
192
|
+
bun test && bun run typecheck && bun run lint
|
|
193
|
+
|
|
194
|
+
# TypeScript/JavaScript (npm)
|
|
195
|
+
npm test && npm run typecheck && npm run lint
|
|
196
|
+
|
|
197
|
+
# TypeScript/JavaScript (Deno)
|
|
198
|
+
deno test && deno check . && deno lint
|
|
199
|
+
|
|
200
|
+
# Python
|
|
201
|
+
pytest && ruff check . && mypy .
|
|
202
|
+
|
|
203
|
+
# Rust
|
|
204
|
+
cargo test && cargo check && cargo clippy --all-targets
|
|
205
|
+
|
|
206
|
+
# Go
|
|
207
|
+
go test ./... && go vet ./... && golangci-lint run
|
|
208
|
+
|
|
209
|
+
# Ruby
|
|
210
|
+
bundle exec rspec && bundle exec rubocop
|
|
211
|
+
|
|
212
|
+
# PHP
|
|
213
|
+
composer test && composer phpstan && composer phpcs
|
|
214
|
+
|
|
215
|
+
# Swift
|
|
216
|
+
swift test && swift build
|
|
217
|
+
|
|
218
|
+
# Elixir
|
|
219
|
+
mix test && mix credo && mix dialyzer
|
|
220
|
+
|
|
221
|
+
# Dart
|
|
222
|
+
dart test && dart analyze
|
|
223
|
+
|
|
224
|
+
# Java (Maven)
|
|
225
|
+
mvn test && mvn compile && mvn checkstyle:check
|
|
226
|
+
|
|
227
|
+
# Java (Gradle)
|
|
228
|
+
./gradlew test && ./gradlew check
|
|
229
|
+
|
|
230
|
+
# C# (.NET)
|
|
231
|
+
dotnet test && dotnet build && dotnet format --verify-no-changes
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Required Files
|
|
235
|
+
|
|
236
|
+
### PRD File (Recommended)
|
|
237
|
+
|
|
238
|
+
Create a `PRD.md` with clear task list:
|
|
239
|
+
|
|
240
|
+
```markdown
|
|
241
|
+
# Migration PRD
|
|
242
|
+
|
|
243
|
+
## Tasks
|
|
244
|
+
|
|
245
|
+
- [ ] Convert test-utils.test.js
|
|
246
|
+
- [ ] Convert api.test.js
|
|
247
|
+
- [ ] Update CI config
|
|
248
|
+
- [ ] Remove Jest dependencies
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Progress File (Agent-maintained)
|
|
252
|
+
|
|
253
|
+
Agent creates/updates `progress.txt`:
|
|
254
|
+
|
|
255
|
+
```markdown
|
|
256
|
+
# Progress Log
|
|
257
|
+
|
|
258
|
+
## Session Started: 2026-01-10
|
|
259
|
+
|
|
260
|
+
### Project: TypeScript with Bun
|
|
261
|
+
|
|
262
|
+
### Commands: bun test | bun run typecheck | bun run lint
|
|
263
|
+
|
|
264
|
+
### Completed Tasks
|
|
265
|
+
|
|
266
|
+
- [x] test-utils.test.js - migrated, 12 tests passing
|
|
267
|
+
- [x] api.test.js - migrated, 8 tests passing
|
|
268
|
+
|
|
269
|
+
### Notes for Next Iteration
|
|
270
|
+
|
|
271
|
+
- CI config needs Vitest runner update
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Completion Signal
|
|
275
|
+
|
|
276
|
+
The loop ends when agent outputs:
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
<promise>COMPLETE</promise>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Best Practices
|
|
283
|
+
|
|
284
|
+
1. **Use LSP for detection**: Check file extensions, not just lock files
|
|
285
|
+
2. **Small steps**: ONE feature per iteration (prevents context rot)
|
|
286
|
+
3. **Quality gates**: Test, typecheck, lint MUST pass before commit
|
|
287
|
+
4. **Prioritize risk**: Hard tasks first, easy wins last
|
|
288
|
+
5. **Track progress**: Update progress.txt every iteration
|
|
289
|
+
6. **Explicit scope**: Vague tasks loop forever
|
|
290
|
+
7. **Graceful fallbacks**: If a command doesn't exist, skip and note it
|
|
291
|
+
|
|
292
|
+
## Troubleshooting
|
|
293
|
+
|
|
294
|
+
| Issue | Solution |
|
|
295
|
+
| -------------------- | ------------------------------------------- |
|
|
296
|
+
| Loop not progressing | Break task into smaller pieces |
|
|
297
|
+
| Tests failing | Fix before continuing, don't skip |
|
|
298
|
+
| Context getting long | Summarize progress, restart session |
|
|
299
|
+
| Stuck on decision | Note in progress.txt, ask user next session |
|
|
300
|
+
| Unknown language | Check file extensions against LSP table |
|
package/package.json
CHANGED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Start Ralph Wiggum autonomous loop for task completion
|
|
3
|
-
argument-hint: "<task> [--prd <file>] [--max <iterations>] [--afk]"
|
|
4
|
-
agent: build
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Ralph Wiggum Loop
|
|
8
|
-
|
|
9
|
-
You are starting a Ralph Wiggum autonomous loop. This pattern enables you to work autonomously on a task list until completion.
|
|
10
|
-
|
|
11
|
-
## Task
|
|
12
|
-
|
|
13
|
-
$ARGUMENTS
|
|
14
|
-
|
|
15
|
-
## Setup
|
|
16
|
-
|
|
17
|
-
1. **Start the loop** by calling the `ralph-start` tool:
|
|
18
|
-
|
|
19
|
-
```typescript
|
|
20
|
-
ralph -
|
|
21
|
-
start({
|
|
22
|
-
task: "$1",
|
|
23
|
-
prdFile: "$2" || null, // Optional: PRD.md, tasks.md, etc.
|
|
24
|
-
progressFile: "progress.txt",
|
|
25
|
-
maxIterations: 50,
|
|
26
|
-
mode: "hitl", // or "afk" for autonomous
|
|
27
|
-
});
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
2. **Create progress.txt** if it doesn't exist:
|
|
31
|
-
|
|
32
|
-
```markdown
|
|
33
|
-
# Progress Log
|
|
34
|
-
|
|
35
|
-
## Session Started: [date]
|
|
36
|
-
|
|
37
|
-
### Completed Tasks
|
|
38
|
-
|
|
39
|
-
(none yet)
|
|
40
|
-
|
|
41
|
-
### Notes for Next Iteration
|
|
42
|
-
|
|
43
|
-
- Starting fresh
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## Loop Behavior
|
|
47
|
-
|
|
48
|
-
After each iteration, the loop will automatically:
|
|
49
|
-
|
|
50
|
-
1. Check if you output `<promise>COMPLETE</promise>`
|
|
51
|
-
2. If yes → Loop ends, success!
|
|
52
|
-
3. If no → Send continuation prompt for next iteration
|
|
53
|
-
4. Repeat until completion or max iterations
|
|
54
|
-
|
|
55
|
-
## Your Instructions
|
|
56
|
-
|
|
57
|
-
For each iteration:
|
|
58
|
-
|
|
59
|
-
1. **Review** the PRD/task list and progress file
|
|
60
|
-
2. **Choose** the highest-priority incomplete task (YOU decide, not first in list)
|
|
61
|
-
3. **Implement** ONE feature only (small steps prevent context rot)
|
|
62
|
-
4. **Validate** with feedback loops:
|
|
63
|
-
- `npm run typecheck` (must pass)
|
|
64
|
-
- `npm run test` (must pass)
|
|
65
|
-
- `npm run lint` (must pass)
|
|
66
|
-
5. **Commit** if all pass
|
|
67
|
-
6. **Update** progress.txt with:
|
|
68
|
-
- Task completed
|
|
69
|
-
- Key decisions made
|
|
70
|
-
- Files changed
|
|
71
|
-
- Notes for next iteration
|
|
72
|
-
|
|
73
|
-
## Exit Conditions
|
|
74
|
-
|
|
75
|
-
Output `<promise>COMPLETE</promise>` when:
|
|
76
|
-
|
|
77
|
-
- ALL tasks in the PRD are complete
|
|
78
|
-
- ALL feedback loops pass
|
|
79
|
-
- Code is committed
|
|
80
|
-
|
|
81
|
-
The loop will also stop if:
|
|
82
|
-
|
|
83
|
-
- Max iterations reached
|
|
84
|
-
- You call `ralph-stop` tool
|
|
85
|
-
- An error occurs
|
|
86
|
-
|
|
87
|
-
## Best Practices
|
|
88
|
-
|
|
89
|
-
- **Small steps**: One feature per iteration
|
|
90
|
-
- **Quality over speed**: Never skip tests
|
|
91
|
-
- **Explicit scope**: Vague tasks loop forever
|
|
92
|
-
- **Track progress**: Update progress.txt every iteration
|
|
93
|
-
- **Prioritize risk**: Hard tasks first, easy wins last
|
|
94
|
-
|
|
95
|
-
## Start Now
|
|
96
|
-
|
|
97
|
-
Call `ralph-start` with the task description to begin the loop.
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenCode Handoff Plugin
|
|
3
|
-
* Injects the most recent handoff file into session compaction
|
|
4
|
-
*
|
|
5
|
-
* Workflow:
|
|
6
|
-
* 1. User creates handoff markdown file in .opencode/memory/handoffs/
|
|
7
|
-
* 2. Session compaction (Ctrl+K) includes handoff context
|
|
8
|
-
* 3. New session resumes from previous state
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { Plugin } from "@opencode-ai/plugin";
|
|
12
|
-
|
|
13
|
-
export const HandoffPlugin: Plugin = async ({ $, directory }) => {
|
|
14
|
-
const HANDOFF_DIR = `${directory}/.opencode/memory/handoffs`;
|
|
15
|
-
|
|
16
|
-
return {
|
|
17
|
-
"experimental.session.compacting": async (_input, output) => {
|
|
18
|
-
// Find most recent handoff file
|
|
19
|
-
const result =
|
|
20
|
-
await $`ls -t ${HANDOFF_DIR}/*.md 2>/dev/null | head -1`.quiet();
|
|
21
|
-
|
|
22
|
-
if (!result.stdout) return;
|
|
23
|
-
|
|
24
|
-
const handoffPath = result.stdout.toString().trim();
|
|
25
|
-
const handoffContent = await $`cat ${handoffPath}`.text();
|
|
26
|
-
|
|
27
|
-
// Inject into compaction context
|
|
28
|
-
output.context.push(`
|
|
29
|
-
## Previous Session Handoff
|
|
30
|
-
|
|
31
|
-
${handoffContent}
|
|
32
|
-
|
|
33
|
-
**IMPORTANT**: Resume work from where previous session left off.
|
|
34
|
-
`);
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
};
|
|
@@ -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
|
-
});
|