opencode-ralph-rlm 0.1.13 → 0.1.14
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/README.md +36 -42
- package/dist/ralph-rlm.js +88 -114
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,8 +6,8 @@ New here? Start with [`GETTINGSTARTEDGUIDE.md`](GETTINGSTARTEDGUIDE.md).
|
|
|
6
6
|
|
|
7
7
|
Two techniques combine to make this work:
|
|
8
8
|
|
|
9
|
-
- **Ralph**
|
|
10
|
-
- **RLM** (Recursive Language Model worker)
|
|
9
|
+
- **Ralph** - the main session acting as strategist+supervisor. It reviews what failed, adjusts the plan and instructions, then delegates coding to a worker. It never writes code itself.
|
|
10
|
+
- **RLM** (Recursive Language Model worker) - a file-first coding session based on [arXiv:2512.24601](https://arxiv.org/abs/2512.24601). Each attempt gets a clean context window and loads all state from files rather than inheriting noise from prior turns.
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
## The problem this solves
|
|
@@ -43,9 +43,9 @@ Context windows are session-local and finite. Files are persistent, inspectable,
|
|
|
43
43
|
|
|
44
44
|
### Separation of strategy and execution
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
Ralph keeps strategy and execution separate: **you (main session)** handle strategy and delegation, while **workers** implement. When a session that just wrote failing code is also responsible for diagnosing *why* it failed and planning the next approach, it pattern-matches against its own failed reasoning. It proposes variations on what didn't work rather than stepping back.
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
In this mode, the *worker* always gets a fresh window. It reads the failure record cold, without the accumulated baggage of having written the code. This mirrors how experienced engineering teams work: the reviewer of a failing PR is often not the one who writes the fix.
|
|
49
49
|
|
|
50
50
|
### The verify contract
|
|
51
51
|
|
|
@@ -63,52 +63,48 @@ The RLM paper demonstrates that full-file reads are expensive and often counterp
|
|
|
63
63
|
|
|
64
64
|
`NOTES_AND_LEARNINGS.md` and `RLM_INSTRUCTIONS.md` are the loop's long-term memory. They survive context resets and accumulate across attempts. The loop doesn't just retry — it gets smarter with each failure.
|
|
65
65
|
|
|
66
|
-
`RLM_INSTRUCTIONS.md` is the inner loop's operating manual. The
|
|
66
|
+
`RLM_INSTRUCTIONS.md` is the inner loop's operating manual. The main strategist (you) updates it between attempts when a pattern of failures reveals a gap in guidance. By attempt 10, the instructions encode everything learned from attempts 1-9.
|
|
67
67
|
|
|
68
68
|
This is why the approach scales to overnight runs. A fresh worker in attempt 10 starts with the accumulated knowledge of 9 prior attempts, encoded in protocol files, without the accumulated noise.
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
## How it works
|
|
72
72
|
|
|
73
|
-
###
|
|
73
|
+
### Two-level architecture (main session = strategist + supervisor)
|
|
74
74
|
|
|
75
75
|
```
|
|
76
|
-
You → main session (
|
|
76
|
+
You → main session (supervisor + strategist in one)
|
|
77
77
|
│
|
|
78
78
|
├─ attempt 1:
|
|
79
|
-
│ ├─
|
|
80
|
-
│
|
|
81
|
-
│ │ → ralph_spawn_worker() → STOP
|
|
79
|
+
│ ├─ strategist (you): ralph_load_context() → review failures → update PLAN.md
|
|
80
|
+
│ ├─ ralph_spawn_worker() → spawns RLM worker session W1
|
|
82
81
|
│ │
|
|
83
|
-
│ └─
|
|
82
|
+
│ └─ RLM worker session W1 ← fresh context
|
|
84
83
|
│ W1: ralph_load_context() → code → ralph_verify() → STOP
|
|
85
84
|
│
|
|
86
85
|
├─ plugin verifies on W1 idle
|
|
87
86
|
│ fail → roll state files → spawn attempt 2
|
|
88
87
|
│
|
|
89
88
|
├─ attempt 2:
|
|
90
|
-
│ ├─
|
|
91
|
-
│
|
|
92
|
-
│ │ → ralph_spawn_worker() → STOP
|
|
89
|
+
│ ├─ strategist (you): reads AGENT_CONTEXT_FOR_NEXT_RALPH.md → adjusts strategy
|
|
90
|
+
│ ├─ ralph_spawn_worker() → spawns RLM worker session W2
|
|
93
91
|
│ │
|
|
94
|
-
│ └─
|
|
92
|
+
│ └─ RLM worker session W2 ← fresh context
|
|
95
93
|
│ W2: loads compact state from files → code → STOP
|
|
96
94
|
│
|
|
97
95
|
└─ pass → done toast
|
|
98
96
|
```
|
|
99
97
|
|
|
100
|
-
Each
|
|
98
|
+
Each role has a distinct purpose and **fresh context window** where applicable:
|
|
101
99
|
|
|
102
100
|
| Role | Session | Context | Responsibility |
|
|
103
101
|
|---|---|---|---|
|
|
104
|
-
| **main** | Your conversation | Persistent |
|
|
105
|
-
| **ralph** | Per-attempt strategist | Fresh | Review failure, update PLAN.md / RLM_INSTRUCTIONS.md, call `ralph_spawn_worker()`. |
|
|
102
|
+
| **main** | Your conversation | Persistent | Supervisor + strategist. Review failures, update PLAN.md / RLM_INSTRUCTIONS.md, call `ralph_spawn_worker()`. |
|
|
106
103
|
| **worker** | Per-attempt coder | Fresh | `ralph_load_context()` → code → `ralph_verify()` → stop. |
|
|
107
104
|
|
|
108
105
|
### Roles and responsibilities (quick map)
|
|
109
106
|
|
|
110
|
-
- **Supervisor (main session):** orchestration and
|
|
111
|
-
- **Ralph strategist:** updates plan/instructions and delegates to a worker. No direct implementation.
|
|
107
|
+
- **Supervisor+strategist (main session):** orchestration, planning, and delegation. Never edits files or runs code directly.
|
|
112
108
|
- **RLM worker:** does the actual coding and verification for this attempt. One pass per session.
|
|
113
109
|
- **Sub-agent:** narrow task helper; updates its own state files under `.opencode/agents/<name>/`.
|
|
114
110
|
|
|
@@ -116,15 +112,13 @@ Each session role has a distinct purpose and **fresh context window**:
|
|
|
116
112
|
|
|
117
113
|
```
|
|
118
114
|
main idle
|
|
119
|
-
└─
|
|
120
|
-
└─
|
|
121
|
-
└─
|
|
122
|
-
└─
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
└─
|
|
126
|
-
└─ spawn Ralph(2)
|
|
127
|
-
└─ (repeat)
|
|
115
|
+
└─ strategist (you) calls ralph_spawn_worker()
|
|
116
|
+
└─ spawn Worker(1)
|
|
117
|
+
└─ Worker(1) calls ralph_verify() and goes idle
|
|
118
|
+
└─ plugin runs verify
|
|
119
|
+
├─ pass → done
|
|
120
|
+
└─ fail → roll state files
|
|
121
|
+
└─ strategist (you) handles next attempt
|
|
128
122
|
```
|
|
129
123
|
|
|
130
124
|
The plugin drives the loop from `session.idle` events. Neither Ralph nor the worker need to know about the outer loop — they just load context, do their job, and stop.
|
|
@@ -295,7 +289,7 @@ This repo now includes project-local agent files under `.opencode/agents/`:
|
|
|
295
289
|
- `.opencode/agents/security-auditor.md`
|
|
296
290
|
|
|
297
291
|
These profiles intentionally keep loop ownership in `ralph-rlm`.
|
|
298
|
-
Do not model
|
|
292
|
+
Do not model the strategist/worker roles as OpenCode primary/subagent replacements.
|
|
299
293
|
|
|
300
294
|
|
|
301
295
|
## Protocol files
|
|
@@ -524,9 +518,9 @@ Run the configured verify command. Returns `{ verdict: "pass"|"fail"|"unknown",
|
|
|
524
518
|
|
|
525
519
|
#### `ralph_spawn_worker()`
|
|
526
520
|
|
|
527
|
-
**
|
|
521
|
+
**Main strategist only.** Spawn a fresh RLM worker session for this attempt. Call this after reviewing protocol files and optionally updating `PLAN.md` / `RLM_INSTRUCTIONS.md`. Then stop — the plugin handles verification and prompts you for the next attempt if needed.
|
|
528
522
|
|
|
529
|
-
If you call this from
|
|
523
|
+
If you call this from an unbound session you will get: `ralph_spawn_worker() must be called from the bound supervisor session.` Bind first with `ralph_create_supervisor_session()`.
|
|
530
524
|
|
|
531
525
|
### Sub-agents
|
|
532
526
|
|
|
@@ -548,7 +542,7 @@ List all sub-agents registered in the current session with their name, goal, sta
|
|
|
548
542
|
|
|
549
543
|
### Supervisor communication
|
|
550
544
|
|
|
551
|
-
These tools let spawned sessions (
|
|
545
|
+
These tools let spawned sessions (RLM worker + sub-agents) communicate back to the main conversation at runtime. State is carried in `.opencode/pending_input.json` for question/response pairs, `SUPERVISOR_LOG.md` for structured status entries, and `CONVERSATION.md` for the readable event timeline.
|
|
552
546
|
|
|
553
547
|
User answers to `ralph_ask()` are persisted too: when you reply via `ralph_respond()`, the response is appended to `CONVERSATION.md`.
|
|
554
548
|
|
|
@@ -655,8 +649,8 @@ RALPH_BOOTSTRAP_RLM_INSTRUCTIONS="@/home/user/prompts/rlm-instructions.md"
|
|
|
655
649
|
| `RALPH_CONTEXT_GATE_ERROR` | — | Error message thrown when the agent tries a destructive tool before loading context. |
|
|
656
650
|
| `RALPH_WORKER_SYSTEM_PROMPT` | — | System prompt injected into every RLM worker session. Describes the one-pass contract. |
|
|
657
651
|
| `RALPH_WORKER_PROMPT` | `{{attempt}}` | Initial prompt sent to each spawned RLM worker session. |
|
|
658
|
-
| `RALPH_SESSION_SYSTEM_PROMPT` | — |
|
|
659
|
-
| `RALPH_SESSION_PROMPT` | `{{attempt}}` |
|
|
652
|
+
| `RALPH_SESSION_SYSTEM_PROMPT` | — | Legacy: system prompt for separate strategist sessions (unused in main-as-strategist mode). |
|
|
653
|
+
| `RALPH_SESSION_PROMPT` | `{{attempt}}` | Prompt sent to the main strategist session when an attempt starts. |
|
|
660
654
|
|
|
661
655
|
### Example: custom continue prompt from a file
|
|
662
656
|
|
|
@@ -699,7 +693,7 @@ Set `maxAttempts` high (25–50), write a detailed `PLAN.md` with a precise defi
|
|
|
699
693
|
|
|
700
694
|
1. Make an attempt.
|
|
701
695
|
2. Run verify.
|
|
702
|
-
3. On failure: roll state,
|
|
696
|
+
3. On failure: roll state, prompt the strategist (you) to diagnose and adjust, spawn the next worker.
|
|
703
697
|
4. Repeat until it passes or hits `maxAttempts`.
|
|
704
698
|
|
|
705
699
|
In the morning, check `SUPERVISOR_LOG.md` and `CONVERSATION.md` for the progress feed, `NOTES_AND_LEARNINGS.md` for what the loop learned, and `AGENT_CONTEXT_FOR_NEXT_RALPH.md` for where it stopped.
|
|
@@ -746,19 +740,19 @@ Parent agent:
|
|
|
746
740
|
|
|
747
741
|
Edit `RLM_INSTRUCTIONS.md` to add project-specific playbooks, register MCP tools, or adjust the debug workflow. Changes persist across attempts. Use `ralph_update_rlm_instructions()` from within a session, or edit the file directly.
|
|
748
742
|
|
|
749
|
-
The instructions file is the primary lever for improving loop performance. If the loop keeps making the same mistake, add a rule. If it keeps following an inefficient path, add a playbook. The
|
|
743
|
+
The instructions file is the primary lever for improving loop performance. If the loop keeps making the same mistake, add a rule. If it keeps following an inefficient path, add a playbook. The main strategist (you) updates these instructions between attempts based on what it observes in the failure record.
|
|
750
744
|
|
|
751
745
|
|
|
752
746
|
## Hooks installed
|
|
753
747
|
|
|
754
748
|
| Hook | What it does |
|
|
755
749
|
|---|---|
|
|
756
|
-
| `event: session.idle` | Routes idle events: **worker** → `handleWorkerIdle` (verify + continue loop); **
|
|
757
|
-
| `event: session.created` | Pre-allocates session state for known worker
|
|
750
|
+
| `event: session.idle` | Routes idle events: **worker** → `handleWorkerIdle` (verify + continue loop); **main/other** → `handleMainIdle` (kick off attempt 1). Also emits heartbeat/staleness warnings and supervisor status updates to `SUPERVISOR_LOG.md` and `CONVERSATION.md`. |
|
|
751
|
+
| `event: session.created` | Pre-allocates session state for known worker sessions. |
|
|
758
752
|
| `event: session.status` | Refreshes heartbeat/progress timestamps for active sessions and surfaces explicit session error statuses to the supervisor feed. |
|
|
759
|
-
| `experimental.chat.system.transform` |
|
|
753
|
+
| `experimental.chat.system.transform` | Two-way routing: **worker** → RLM file-first prompt; **main/other** → supervisor+strategist prompt. |
|
|
760
754
|
| `experimental.session.compacting` | Injects protocol file pointers into compaction context so state survives context compression. |
|
|
761
|
-
| `tool.execute.before` | Blocks destructive tools (`write`, `edit`, `bash`, `delete`, `move`, `rename`) in **worker and sub-agent sessions** until `ralph_load_context()` has been called.
|
|
755
|
+
| `tool.execute.before` | Blocks destructive tools (`write`, `edit`, `bash`, `delete`, `move`, `rename`) in **worker and sub-agent sessions** until `ralph_load_context()` has been called. |
|
|
762
756
|
|
|
763
757
|
|
|
764
758
|
## Background
|
|
@@ -767,7 +761,7 @@ The instructions file is the primary lever for improving loop performance. If th
|
|
|
767
761
|
|
|
768
762
|
The outer loop is named after the [Ralph Wiggum technique](https://www.geoffreyhuntley.com/ralph) — a `while` loop that feeds a prompt to an AI agent until it succeeds. The name reflects the philosophy: persistent, not clever. The loop doesn't try to be smart about when to give up. It tries, records what happened, and tries again with better instructions.
|
|
769
763
|
|
|
770
|
-
The key addition in this plugin over a naive Ralph implementation is the **separation of the strategist from the worker**.
|
|
764
|
+
The key addition in this plugin over a naive Ralph implementation is the **separation of the strategist from the worker**. The main session handles strategy and delegation, while each worker gets a fresh context to implement. This keeps planning clean while still benefiting from fresh worker windows.
|
|
771
765
|
|
|
772
766
|
### The RLM inner loop
|
|
773
767
|
|
package/dist/ralph-rlm.js
CHANGED
|
@@ -23510,9 +23510,9 @@ var DEFAULT_TEMPLATES = {
|
|
|
23510
23510
|
systemPrompt: [
|
|
23511
23511
|
"RALPH SUPERVISOR:",
|
|
23512
23512
|
"- You are the Ralph supervisor. You orchestrate RLM worker sessions; you do NOT write code yourself.",
|
|
23513
|
-
"- When the user gives you a goal, describe the task briefly and
|
|
23514
|
-
"- You
|
|
23515
|
-
" Supervisor = orchestration +
|
|
23513
|
+
"- When the user gives you a goal, describe the task briefly and then act as the strategist: call ralph_spawn_worker() to hand off.",
|
|
23514
|
+
"- You ARE the strategist in the main session, and you are NOT the RLM worker.",
|
|
23515
|
+
" Supervisor+strategist = orchestration + planning + delegation; RLM worker = implementation.",
|
|
23516
23516
|
"- Workers are spawned per-attempt with a fresh context window. They load state from protocol files.",
|
|
23517
23517
|
"- Protocol files (PLAN.md, RLM_INSTRUCTIONS.md, etc.) persist across all attempts \u2014 edit them to guide workers.",
|
|
23518
23518
|
"- After each worker attempt the plugin runs verify and either finishes or spawns the next worker.",
|
|
@@ -24290,6 +24290,49 @@ var RalphRLM = async ({ client, $, worktree }) => {
|
|
|
24290
24290
|
return false;
|
|
24291
24291
|
}
|
|
24292
24292
|
};
|
|
24293
|
+
const promptStrategistInMain = async (attempt) => {
|
|
24294
|
+
if (!supervisor.sessionId) {
|
|
24295
|
+
await notifySupervisor("supervisor", "Cannot prompt strategist: supervisor session not bound. Run ralph_create_supervisor_session().", "warning", true);
|
|
24296
|
+
return;
|
|
24297
|
+
}
|
|
24298
|
+
supervisor.awaitingStrategist = true;
|
|
24299
|
+
if (supervisor.ralphHandoffAttempt !== attempt) {
|
|
24300
|
+
supervisor.ralphHandoffAttempt = attempt;
|
|
24301
|
+
supervisor.ralphHandoffRetries = 0;
|
|
24302
|
+
}
|
|
24303
|
+
const promptText = interpolate(templates.ralphSessionPrompt, {
|
|
24304
|
+
attempt: String(attempt),
|
|
24305
|
+
nextAttempt: String(attempt + 1)
|
|
24306
|
+
});
|
|
24307
|
+
const ok = await sendPromptWithFallback(supervisor.sessionId, promptText, `Strategist prompt (attempt ${attempt})`, supervisor.sessionId);
|
|
24308
|
+
if (!ok) {
|
|
24309
|
+
supervisor.paused = true;
|
|
24310
|
+
await notifySupervisor(`supervisor/attempt-${attempt}`, "Strategist prompt failed; supervision paused. Retry with ralph_create_supervisor_session(restart_if_done=true).", "error", true, supervisor.sessionId);
|
|
24311
|
+
return;
|
|
24312
|
+
}
|
|
24313
|
+
if (supervisor.ralphHandoffTimer) {
|
|
24314
|
+
clearTimeout(supervisor.ralphHandoffTimer);
|
|
24315
|
+
}
|
|
24316
|
+
const cfg = await run(getConfig());
|
|
24317
|
+
const timeoutMs = Math.max(cfg.strategistHandoffMinutes, 1) * 60000;
|
|
24318
|
+
supervisor.ralphHandoffTimer = setTimeout(async () => {
|
|
24319
|
+
if (supervisor.done || supervisor.paused)
|
|
24320
|
+
return;
|
|
24321
|
+
if (!supervisor.awaitingStrategist)
|
|
24322
|
+
return;
|
|
24323
|
+
if (supervisor.ralphHandoffAttempt !== attempt)
|
|
24324
|
+
return;
|
|
24325
|
+
const retries = supervisor.ralphHandoffRetries ?? 0;
|
|
24326
|
+
if (retries < cfg.strategistHandoffMaxRetries) {
|
|
24327
|
+
supervisor.ralphHandoffRetries = retries + 1;
|
|
24328
|
+
await notifySupervisor(`supervisor/attempt-${attempt}`, `Strategist did not hand off; re-prompting main session (${retries + 1}/${cfg.strategistHandoffMaxRetries}).`, "warning", true, supervisor.sessionId);
|
|
24329
|
+
await promptStrategistInMain(attempt);
|
|
24330
|
+
return;
|
|
24331
|
+
}
|
|
24332
|
+
await notifySupervisor(`supervisor/attempt-${attempt}`, "Strategist did not hand off after retries; supervision paused. Use ralph_create_supervisor_session(restart_if_done=true) to retry.", "error", true, supervisor.sessionId);
|
|
24333
|
+
supervisor.paused = true;
|
|
24334
|
+
}, timeoutMs);
|
|
24335
|
+
};
|
|
24293
24336
|
const detectProjectDefaults = (root) => exports_Effect.gen(function* () {
|
|
24294
24337
|
const j = (f) => NodePath.join(root, f);
|
|
24295
24338
|
const hasBunLock = (yield* fileExists(j("bun.lockb"))) || (yield* fileExists(j("bun.lock")));
|
|
@@ -25133,7 +25176,7 @@ No pending questions found.`));
|
|
|
25133
25176
|
if (supervisor.done && args2.restart_if_done === true) {
|
|
25134
25177
|
supervisor.done = false;
|
|
25135
25178
|
supervisor.paused = false;
|
|
25136
|
-
supervisor.
|
|
25179
|
+
supervisor.awaitingStrategist = false;
|
|
25137
25180
|
supervisor.currentWorkerSessionId = undefined;
|
|
25138
25181
|
supervisor.activeReviewerName = undefined;
|
|
25139
25182
|
supervisor.activeReviewerAttempt = undefined;
|
|
@@ -25142,19 +25185,19 @@ No pending questions found.`));
|
|
|
25142
25185
|
await persistReviewerState();
|
|
25143
25186
|
await notifySupervisor("supervisor", "Supervisor done-state reset for a new run.", "info", true, sessionID);
|
|
25144
25187
|
}
|
|
25145
|
-
if (supervisor.
|
|
25188
|
+
if (supervisor.currentWorkerSessionId || supervisor.done) {
|
|
25146
25189
|
return JSON.stringify({
|
|
25147
25190
|
ok: true,
|
|
25148
25191
|
started: false,
|
|
25149
25192
|
message: "Loop is already running or completed for this process.",
|
|
25150
|
-
currentRalphSessionId: supervisor.currentRalphSessionId,
|
|
25151
25193
|
currentWorkerSessionId: supervisor.currentWorkerSessionId,
|
|
25152
25194
|
done: supervisor.done
|
|
25153
25195
|
}, null, 2);
|
|
25154
25196
|
}
|
|
25155
25197
|
supervisor.attempt = 1;
|
|
25198
|
+
supervisor.awaitingStrategist = false;
|
|
25156
25199
|
await notifySupervisor("supervisor", "Starting Ralph loop at attempt 1 (manual start).", "info", true, sessionID);
|
|
25157
|
-
await
|
|
25200
|
+
await promptStrategistInMain(1);
|
|
25158
25201
|
return JSON.stringify({ ok: true, started: true, attempt: 1 }, null, 2);
|
|
25159
25202
|
}
|
|
25160
25203
|
});
|
|
@@ -25170,6 +25213,7 @@ No pending questions found.`));
|
|
|
25170
25213
|
const reason = args2.reason?.trim();
|
|
25171
25214
|
supervisor.done = true;
|
|
25172
25215
|
supervisor.paused = true;
|
|
25216
|
+
supervisor.awaitingStrategist = false;
|
|
25173
25217
|
const sessionsToAbort = Array.from(sessionMap.keys());
|
|
25174
25218
|
for (const id of sessionsToAbort) {
|
|
25175
25219
|
await client.session.abort({ path: { id } }).catch(() => {});
|
|
@@ -25179,7 +25223,6 @@ No pending questions found.`));
|
|
|
25179
25223
|
}
|
|
25180
25224
|
stopAllCommands("supervision-ended");
|
|
25181
25225
|
sessionMap.clear();
|
|
25182
|
-
supervisor.currentRalphSessionId = undefined;
|
|
25183
25226
|
supervisor.currentWorkerSessionId = undefined;
|
|
25184
25227
|
supervisor.activeReviewerName = undefined;
|
|
25185
25228
|
supervisor.activeReviewerAttempt = undefined;
|
|
@@ -25203,7 +25246,7 @@ No pending questions found.`));
|
|
|
25203
25246
|
}
|
|
25204
25247
|
});
|
|
25205
25248
|
const tool_ralph_supervision_status = tool({
|
|
25206
|
-
description: "Get current supervision state (binding, attempt,
|
|
25249
|
+
description: "Get current supervision state (binding, attempt, awaiting strategist/worker, done flag).",
|
|
25207
25250
|
args: {},
|
|
25208
25251
|
async execute(_args, _ctx) {
|
|
25209
25252
|
return JSON.stringify({
|
|
@@ -25212,7 +25255,7 @@ No pending questions found.`));
|
|
|
25212
25255
|
attempt: supervisor.attempt,
|
|
25213
25256
|
done: supervisor.done,
|
|
25214
25257
|
paused: supervisor.paused ?? false,
|
|
25215
|
-
|
|
25258
|
+
awaitingStrategist: supervisor.awaitingStrategist ?? false,
|
|
25216
25259
|
currentWorkerSessionId: supervisor.currentWorkerSessionId ?? null,
|
|
25217
25260
|
activeReviewerName: supervisor.activeReviewerName ?? null,
|
|
25218
25261
|
activeReviewerAttempt: supervisor.activeReviewerAttempt ?? null,
|
|
@@ -25258,11 +25301,11 @@ No pending questions found.`));
|
|
|
25258
25301
|
message: "Loop is marked done. Use ralph_create_supervisor_session(restart_if_done=true)."
|
|
25259
25302
|
}, null, 2);
|
|
25260
25303
|
}
|
|
25261
|
-
if (supervisor.
|
|
25304
|
+
if (supervisor.currentWorkerSessionId) {
|
|
25262
25305
|
return JSON.stringify({ ok: true, resumed: true, started: false, message: "Loop already running." }, null, 2);
|
|
25263
25306
|
}
|
|
25264
25307
|
supervisor.attempt = Math.max(1, supervisor.attempt || 1);
|
|
25265
|
-
await
|
|
25308
|
+
await promptStrategistInMain(supervisor.attempt);
|
|
25266
25309
|
return JSON.stringify({ ok: true, resumed: true, started: true, attempt: supervisor.attempt }, null, 2);
|
|
25267
25310
|
}
|
|
25268
25311
|
});
|
|
@@ -25348,8 +25391,14 @@ Set a new goal and run again.
|
|
|
25348
25391
|
supervisor.done = false;
|
|
25349
25392
|
supervisor.paused = false;
|
|
25350
25393
|
supervisor.attempt = 0;
|
|
25351
|
-
supervisor.currentRalphSessionId = undefined;
|
|
25352
25394
|
supervisor.currentWorkerSessionId = undefined;
|
|
25395
|
+
supervisor.awaitingStrategist = false;
|
|
25396
|
+
supervisor.ralphHandoffRetries = 0;
|
|
25397
|
+
supervisor.ralphHandoffAttempt = undefined;
|
|
25398
|
+
if (supervisor.ralphHandoffTimer) {
|
|
25399
|
+
clearTimeout(supervisor.ralphHandoffTimer);
|
|
25400
|
+
supervisor.ralphHandoffTimer = undefined;
|
|
25401
|
+
}
|
|
25353
25402
|
supervisor.activeReviewerName = undefined;
|
|
25354
25403
|
supervisor.activeReviewerAttempt = undefined;
|
|
25355
25404
|
supervisor.activeReviewerSessionId = undefined;
|
|
@@ -25439,7 +25488,7 @@ Set a new goal and run again.
|
|
|
25439
25488
|
supervisor.done = false;
|
|
25440
25489
|
supervisor.paused = false;
|
|
25441
25490
|
supervisor.attempt = 1;
|
|
25442
|
-
await
|
|
25491
|
+
await promptStrategistInMain(1);
|
|
25443
25492
|
actions.push("Started loop at attempt 1");
|
|
25444
25493
|
}
|
|
25445
25494
|
await appendConversationEntry("supervisor", `Quickstart completed for goal: ${args2.goal}`);
|
|
@@ -25624,18 +25673,12 @@ Set a new goal and run again.
|
|
|
25624
25673
|
s.lastProgressAt = now2;
|
|
25625
25674
|
});
|
|
25626
25675
|
};
|
|
25627
|
-
await maybeWarn(supervisor.currentRalphSessionId, "Strategist");
|
|
25628
25676
|
await maybeWarn(supervisor.currentWorkerSessionId, "Worker");
|
|
25629
25677
|
};
|
|
25630
25678
|
const clearSessionTracking = async (sessionId, reason) => {
|
|
25631
25679
|
const st = sessionMap.get(sessionId);
|
|
25632
25680
|
sessionMap.delete(sessionId);
|
|
25633
25681
|
let didUpdate = false;
|
|
25634
|
-
const clearedRalph = supervisor.currentRalphSessionId === sessionId;
|
|
25635
|
-
if (clearedRalph) {
|
|
25636
|
-
supervisor.currentRalphSessionId = undefined;
|
|
25637
|
-
didUpdate = true;
|
|
25638
|
-
}
|
|
25639
25682
|
if (supervisor.currentWorkerSessionId === sessionId) {
|
|
25640
25683
|
supervisor.currentWorkerSessionId = undefined;
|
|
25641
25684
|
didUpdate = true;
|
|
@@ -25648,7 +25691,7 @@ Set a new goal and run again.
|
|
|
25648
25691
|
didUpdate = true;
|
|
25649
25692
|
await persistReviewerState();
|
|
25650
25693
|
}
|
|
25651
|
-
if (supervisor.ralphHandoffTimer &&
|
|
25694
|
+
if (supervisor.ralphHandoffTimer && supervisor.sessionId === sessionId) {
|
|
25652
25695
|
clearTimeout(supervisor.ralphHandoffTimer);
|
|
25653
25696
|
supervisor.ralphHandoffTimer = undefined;
|
|
25654
25697
|
}
|
|
@@ -25730,77 +25773,30 @@ ${interpolate(templates.continuePrompt, { attempt: String(attemptN), verdict })}
|
|
|
25730
25773
|
tool_ralph_spawn_worker_impl = async (_args, ctx) => {
|
|
25731
25774
|
const sessionID = ctx.sessionID ?? "";
|
|
25732
25775
|
const st = sessionMap.get(sessionID);
|
|
25733
|
-
|
|
25734
|
-
|
|
25776
|
+
const isMainStrategist = st?.role === "main";
|
|
25777
|
+
if (!isMainStrategist && st?.role !== "ralph") {
|
|
25778
|
+
throw new Error("ralph_spawn_worker() must be called from the main strategist session.");
|
|
25735
25779
|
}
|
|
25736
|
-
if (
|
|
25737
|
-
throw new Error("ralph_spawn_worker()
|
|
25780
|
+
if (isMainStrategist && supervisor.sessionId && supervisor.sessionId !== sessionID) {
|
|
25781
|
+
throw new Error("ralph_spawn_worker() must be called from the bound supervisor session.");
|
|
25782
|
+
}
|
|
25783
|
+
if (supervisor.attempt < 1) {
|
|
25784
|
+
throw new Error("No active attempt. Start with ralph_create_supervisor_session(start_loop=true).");
|
|
25738
25785
|
}
|
|
25739
25786
|
if (supervisor.ralphHandoffTimer) {
|
|
25740
25787
|
clearTimeout(supervisor.ralphHandoffTimer);
|
|
25741
25788
|
supervisor.ralphHandoffTimer = undefined;
|
|
25742
25789
|
}
|
|
25743
|
-
|
|
25790
|
+
supervisor.awaitingStrategist = false;
|
|
25791
|
+
const attempt = isMainStrategist ? supervisor.attempt : st.attempt;
|
|
25792
|
+
const workerId = await spawnRlmWorker(attempt);
|
|
25744
25793
|
mutateSession(sessionID, (s) => {
|
|
25745
25794
|
s.workerSpawned = true;
|
|
25746
25795
|
});
|
|
25747
25796
|
supervisor.ralphHandoffRetries = 0;
|
|
25748
|
-
supervisor.ralphHandoffAttempt =
|
|
25749
|
-
await notifySupervisor(`
|
|
25750
|
-
return JSON.stringify({ ok: true, workerSessionId: workerId, attempt
|
|
25751
|
-
};
|
|
25752
|
-
const spawnRalphSession = async (attempt) => {
|
|
25753
|
-
const result = await client.session.create({
|
|
25754
|
-
body: { title: `ralph-strategist-attempt-${attempt}` }
|
|
25755
|
-
});
|
|
25756
|
-
const ralphId = result.data?.id ?? `ralph-${Date.now()}`;
|
|
25757
|
-
if (supervisor.ralphHandoffAttempt !== attempt) {
|
|
25758
|
-
supervisor.ralphHandoffAttempt = attempt;
|
|
25759
|
-
supervisor.ralphHandoffRetries = 0;
|
|
25760
|
-
}
|
|
25761
|
-
supervisor.currentRalphSessionId = ralphId;
|
|
25762
|
-
sessionMap.set(ralphId, freshSession("ralph", attempt));
|
|
25763
|
-
mutateSession(ralphId, (s) => {
|
|
25764
|
-
s.lastProgressAt = Date.now();
|
|
25765
|
-
});
|
|
25766
|
-
const promptText = interpolate(templates.ralphSessionPrompt, {
|
|
25767
|
-
attempt: String(attempt),
|
|
25768
|
-
nextAttempt: String(attempt + 1)
|
|
25769
|
-
});
|
|
25770
|
-
const promptOk = await sendPromptWithFallback(ralphId, promptText, `Strategist prompt (attempt ${attempt})`, ralphId);
|
|
25771
|
-
if (!promptOk) {
|
|
25772
|
-
supervisor.currentRalphSessionId = undefined;
|
|
25773
|
-
await client.session.abort({ path: { id: ralphId } }).catch(() => {});
|
|
25774
|
-
await notifySupervisor(`supervisor/attempt-${attempt}`, "Strategist prompt failed; supervision paused. Retry with ralph_create_supervisor_session(restart_if_done=true).", "error", true);
|
|
25775
|
-
supervisor.paused = true;
|
|
25776
|
-
return;
|
|
25777
|
-
}
|
|
25778
|
-
if (supervisor.ralphHandoffTimer) {
|
|
25779
|
-
clearTimeout(supervisor.ralphHandoffTimer);
|
|
25780
|
-
}
|
|
25781
|
-
const cfg = await run(getConfig());
|
|
25782
|
-
const timeoutMs = Math.max(cfg.strategistHandoffMinutes, 1) * 60000;
|
|
25783
|
-
supervisor.ralphHandoffTimer = setTimeout(async () => {
|
|
25784
|
-
if (supervisor.done || supervisor.paused)
|
|
25785
|
-
return;
|
|
25786
|
-
if (supervisor.currentRalphSessionId !== ralphId)
|
|
25787
|
-
return;
|
|
25788
|
-
const st = sessionMap.get(ralphId);
|
|
25789
|
-
if (st?.workerSpawned)
|
|
25790
|
-
return;
|
|
25791
|
-
const retries = supervisor.ralphHandoffRetries ?? 0;
|
|
25792
|
-
if (retries < cfg.strategistHandoffMaxRetries) {
|
|
25793
|
-
supervisor.ralphHandoffRetries = retries + 1;
|
|
25794
|
-
await notifySupervisor(`ralph/attempt-${attempt}`, `Strategist did not hand off; retrying strategist (${retries + 1}/${cfg.strategistHandoffMaxRetries}).`, "warning", true, ralphId);
|
|
25795
|
-
supervisor.currentRalphSessionId = undefined;
|
|
25796
|
-
await client.session.abort({ path: { id: ralphId } }).catch(() => {});
|
|
25797
|
-
await spawnRalphSession(attempt);
|
|
25798
|
-
return;
|
|
25799
|
-
}
|
|
25800
|
-
await notifySupervisor(`ralph/attempt-${attempt}`, "Strategist did not hand off after retries; supervision paused. Use ralph_create_supervisor_session(restart_if_done=true) to retry.", "error", true, ralphId);
|
|
25801
|
-
supervisor.paused = true;
|
|
25802
|
-
}, timeoutMs);
|
|
25803
|
-
await notifySupervisor(`supervisor/attempt-${attempt}`, `Spawned Ralph strategist session ${ralphId}.`, "info", true);
|
|
25797
|
+
supervisor.ralphHandoffAttempt = attempt;
|
|
25798
|
+
await notifySupervisor(`supervisor/attempt-${attempt}`, `Delegated coding to worker session ${workerId}.`, "info", true, sessionID);
|
|
25799
|
+
return JSON.stringify({ ok: true, workerSessionId: workerId, attempt }, null, 2);
|
|
25804
25800
|
};
|
|
25805
25801
|
const handleWorkerIdle = async (workerSessionId) => {
|
|
25806
25802
|
if (supervisor.currentWorkerSessionId !== workerSessionId)
|
|
@@ -25819,6 +25815,11 @@ ${interpolate(templates.continuePrompt, { attempt: String(attemptN), verdict })}
|
|
|
25819
25815
|
const { verdict, details } = await runAndParseVerify();
|
|
25820
25816
|
if (verdict === "pass") {
|
|
25821
25817
|
supervisor.done = true;
|
|
25818
|
+
supervisor.awaitingStrategist = false;
|
|
25819
|
+
if (supervisor.ralphHandoffTimer) {
|
|
25820
|
+
clearTimeout(supervisor.ralphHandoffTimer);
|
|
25821
|
+
supervisor.ralphHandoffTimer = undefined;
|
|
25822
|
+
}
|
|
25822
25823
|
await run(writeFile(NodePath.join(worktree, FILES.NEXT_RALPH), interpolate(templates.doneFileContent, { timestamp: nowISO() })));
|
|
25823
25824
|
await client.tui.showToast({
|
|
25824
25825
|
body: { title: "Ralph: Done", message: "Verification passed. Loop complete.", variant: "success" }
|
|
@@ -25840,38 +25841,17 @@ ${interpolate(templates.continuePrompt, { attempt: String(attemptN), verdict })}
|
|
|
25840
25841
|
await notifySupervisor(`worker/attempt-${supervisor.attempt}`, `Verification ${verdict}. Preparing next attempt.`, verdict === "fail" ? "warning" : "info", true, workerSessionId);
|
|
25841
25842
|
supervisor.attempt += 1;
|
|
25842
25843
|
await rolloverState(supervisor.attempt - 1, verdict, details);
|
|
25843
|
-
await
|
|
25844
|
-
};
|
|
25845
|
-
const handleRalphSessionIdle = async (ralphSessionId) => {
|
|
25846
|
-
if (supervisor.currentRalphSessionId !== ralphSessionId)
|
|
25847
|
-
return;
|
|
25848
|
-
if (supervisor.done)
|
|
25849
|
-
return;
|
|
25850
|
-
supervisor.currentRalphSessionId = undefined;
|
|
25851
|
-
const st = sessionMap.get(ralphSessionId);
|
|
25852
|
-
if (st && !st.reportedStatus) {
|
|
25853
|
-
await notifySupervisor(`ralph/attempt-${st.attempt}`, "No explicit strategist status reported before idle.", "info", true, ralphSessionId);
|
|
25854
|
-
}
|
|
25855
|
-
if (!st?.workerSpawned) {
|
|
25856
|
-
await client.tui.showToast({
|
|
25857
|
-
body: {
|
|
25858
|
-
title: "Ralph: no worker spawned",
|
|
25859
|
-
message: `Ralph session for attempt ${st?.attempt ?? supervisor.attempt} ended without calling ralph_spawn_worker().`,
|
|
25860
|
-
variant: "warning"
|
|
25861
|
-
}
|
|
25862
|
-
}).catch(() => {});
|
|
25863
|
-
await notifySupervisor(`ralph/attempt-${st?.attempt ?? supervisor.attempt}`, "Strategist went idle without spawning a worker.", "warning", true, ralphSessionId);
|
|
25864
|
-
}
|
|
25844
|
+
await promptStrategistInMain(supervisor.attempt);
|
|
25865
25845
|
};
|
|
25866
25846
|
const handleMainIdle = async (sessionID) => {
|
|
25867
25847
|
if (supervisor.done)
|
|
25868
25848
|
return;
|
|
25869
25849
|
if (supervisor.paused)
|
|
25870
25850
|
return;
|
|
25871
|
-
if (supervisor.currentRalphSessionId)
|
|
25872
|
-
return;
|
|
25873
25851
|
if (supervisor.currentWorkerSessionId)
|
|
25874
25852
|
return;
|
|
25853
|
+
if (supervisor.awaitingStrategist)
|
|
25854
|
+
return;
|
|
25875
25855
|
const cfg = await run(getConfig());
|
|
25876
25856
|
if (!cfg.enabled)
|
|
25877
25857
|
return;
|
|
@@ -25892,7 +25872,7 @@ ${interpolate(templates.continuePrompt, { attempt: String(attemptN), verdict })}
|
|
|
25892
25872
|
}
|
|
25893
25873
|
supervisor.attempt = 1;
|
|
25894
25874
|
await notifySupervisor("supervisor", "Starting Ralph loop at attempt 1.", "info", true, sessionID);
|
|
25895
|
-
await
|
|
25875
|
+
await promptStrategistInMain(1);
|
|
25896
25876
|
};
|
|
25897
25877
|
return {
|
|
25898
25878
|
tool: {
|
|
@@ -25931,7 +25911,7 @@ ${interpolate(templates.continuePrompt, { attempt: String(attemptN), verdict })}
|
|
|
25931
25911
|
output.system = output.system ?? [];
|
|
25932
25912
|
const sessionID = input.sessionID ?? input.session_id ?? input.session?.id;
|
|
25933
25913
|
const role = sessionMap.get(sessionID ?? "")?.role;
|
|
25934
|
-
const base = role === "worker" || role === "subagent" ? templates.workerSystemPrompt :
|
|
25914
|
+
const base = role === "worker" || role === "subagent" ? templates.workerSystemPrompt : templates.systemPrompt;
|
|
25935
25915
|
const full = templates.systemPromptAppend ? `${base}
|
|
25936
25916
|
${templates.systemPromptAppend}` : base;
|
|
25937
25917
|
output.system.push(full);
|
|
@@ -25973,8 +25953,6 @@ ${templates.systemPromptAppend}` : base;
|
|
|
25973
25953
|
const state = sessionMap.get(sessionID);
|
|
25974
25954
|
if (state?.role === "worker" && supervisor.currentWorkerSessionId !== sessionID)
|
|
25975
25955
|
return;
|
|
25976
|
-
if (state?.role === "ralph" && supervisor.currentRalphSessionId !== sessionID)
|
|
25977
|
-
return;
|
|
25978
25956
|
if (state) {
|
|
25979
25957
|
mutateSession(sessionID, (s) => {
|
|
25980
25958
|
s.lastProgressAt = Date.now();
|
|
@@ -25995,10 +25973,6 @@ ${templates.systemPromptAppend}` : base;
|
|
|
25995
25973
|
await handleWorkerIdle(sessionID).catch((err) => {
|
|
25996
25974
|
appLog("error", "handleWorkerIdle error", { error: String(err), sessionID });
|
|
25997
25975
|
});
|
|
25998
|
-
} else if (supervisor.currentRalphSessionId === sessionID) {
|
|
25999
|
-
await handleRalphSessionIdle(sessionID).catch((err) => {
|
|
26000
|
-
appLog("error", "handleRalphSessionIdle error", { error: String(err), sessionID });
|
|
26001
|
-
});
|
|
26002
25976
|
} else {
|
|
26003
25977
|
await handleMainIdle(sessionID).catch((err) => {
|
|
26004
25978
|
appLog("error", "handleMainIdle error", { error: String(err), sessionID });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-ralph-rlm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "OpenCode plugin: Ralph outer loop + RLM inner loop. Iterative AI development with file-first discipline and sub-agent support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/ralph-rlm.js",
|