opencode-ralph-rlm 0.1.11 → 0.1.13
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 +2 -0
- package/dist/ralph-rlm.js +123 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -228,6 +228,8 @@ Create `.opencode/ralph.json`. All fields are optional — the plugin runs with
|
|
|
228
228
|
| `statusVerbosity` | `"normal"` | Supervisor status emission level: `minimal` (warnings/errors), `normal`, or `verbose`. |
|
|
229
229
|
| `maxAttempts` | `20` | Hard stop after this many failed verify attempts. |
|
|
230
230
|
| `heartbeatMinutes` | `15` | Warn if active strategist/worker has no progress for this many minutes. |
|
|
231
|
+
| `strategistHandoffMinutes` | `5` | Warn/retry if the strategist does not spawn a worker within this many minutes. |
|
|
232
|
+
| `strategistHandoffMaxRetries` | `2` | Max retries to respawn strategist after a missed handoff. |
|
|
231
233
|
| `verifyTimeoutMinutes` | `0` | Timeout for verify command in minutes. `0` disables timeouts. |
|
|
232
234
|
| `verify.command` | - | Shell command to run as an array, e.g. `["bun", "run", "verify"]`. If omitted, verify always returns `unknown`. |
|
|
233
235
|
| `verify.cwd` | `"."` | Working directory for the verify command, relative to the repo root. |
|
package/dist/ralph-rlm.js
CHANGED
|
@@ -23417,6 +23417,8 @@ var RalphConfigSchema = exports_Schema.Struct({
|
|
|
23417
23417
|
statusVerbosity: exports_Schema.optional(exports_Schema.Union(exports_Schema.Literal("minimal"), exports_Schema.Literal("normal"), exports_Schema.Literal("verbose"))),
|
|
23418
23418
|
maxAttempts: exports_Schema.optional(exports_Schema.Number),
|
|
23419
23419
|
heartbeatMinutes: exports_Schema.optional(exports_Schema.Number),
|
|
23420
|
+
strategistHandoffMinutes: exports_Schema.optional(exports_Schema.Number),
|
|
23421
|
+
strategistHandoffMaxRetries: exports_Schema.optional(exports_Schema.Number),
|
|
23420
23422
|
verifyTimeoutMinutes: exports_Schema.optional(exports_Schema.Number),
|
|
23421
23423
|
verify: exports_Schema.optional(VerifyConfigSchema),
|
|
23422
23424
|
gateDestructiveToolsUntilContextLoaded: exports_Schema.optional(exports_Schema.Boolean),
|
|
@@ -23456,6 +23458,8 @@ var CONFIG_DEFAULTS = {
|
|
|
23456
23458
|
statusVerbosity: "normal",
|
|
23457
23459
|
maxAttempts: 20,
|
|
23458
23460
|
heartbeatMinutes: 15,
|
|
23461
|
+
strategistHandoffMinutes: 5,
|
|
23462
|
+
strategistHandoffMaxRetries: 2,
|
|
23459
23463
|
verifyTimeoutMinutes: 0,
|
|
23460
23464
|
gateDestructiveToolsUntilContextLoaded: true,
|
|
23461
23465
|
maxRlmSliceLines: 200,
|
|
@@ -23482,6 +23486,8 @@ function resolveConfig(raw) {
|
|
|
23482
23486
|
statusVerbosity: raw.statusVerbosity ?? CONFIG_DEFAULTS.statusVerbosity,
|
|
23483
23487
|
maxAttempts: toBoundedInt(raw.maxAttempts, CONFIG_DEFAULTS.maxAttempts, 1, 500),
|
|
23484
23488
|
heartbeatMinutes: toBoundedInt(raw.heartbeatMinutes, CONFIG_DEFAULTS.heartbeatMinutes, 1, 240),
|
|
23489
|
+
strategistHandoffMinutes: toBoundedInt(raw.strategistHandoffMinutes, CONFIG_DEFAULTS.strategistHandoffMinutes, 1, 60),
|
|
23490
|
+
strategistHandoffMaxRetries: toBoundedInt(raw.strategistHandoffMaxRetries, CONFIG_DEFAULTS.strategistHandoffMaxRetries, 0, 10),
|
|
23485
23491
|
verifyTimeoutMinutes: toBoundedInt(raw.verifyTimeoutMinutes, CONFIG_DEFAULTS.verifyTimeoutMinutes, 0, 240),
|
|
23486
23492
|
...verify !== undefined ? { verify } : {},
|
|
23487
23493
|
gateDestructiveToolsUntilContextLoaded: raw.gateDestructiveToolsUntilContextLoaded ?? CONFIG_DEFAULTS.gateDestructiveToolsUntilContextLoaded,
|
|
@@ -23514,6 +23520,7 @@ var DEFAULT_TEMPLATES = {
|
|
|
23514
23520
|
" When you receive one, call ralph_respond(id, answer) to unblock the session.",
|
|
23515
23521
|
"- Use ralph_doctor() to check setup, ralph_bootstrap_plan() to generate PLAN/TODOS,",
|
|
23516
23522
|
" ralph_create_supervisor_session() to bind/start explicitly, ralph_pause_supervision()/ralph_resume_supervision() to control execution, and ralph_end_supervision() to stop.",
|
|
23523
|
+
"- Only call loop-control tools (spawn, pause, resume, end) after the supervisor session is bound via ralph_create_supervisor_session().",
|
|
23517
23524
|
"- End supervision when verification has passed and the user confirms they are done, or when the user explicitly asks to stop the loop.",
|
|
23518
23525
|
"- Optional reviewer flow: worker marks readiness with ralph_request_review(); supervisor runs ralph_run_reviewer().",
|
|
23519
23526
|
"- Monitor progress in SUPERVISOR_LOG.md, CONVERSATION.md, or via toast notifications.",
|
|
@@ -23522,7 +23529,22 @@ var DEFAULT_TEMPLATES = {
|
|
|
23522
23529
|
"- The supervisor owns the lifecycle only. It never edits files or runs rlm_grep/rlm_slice.",
|
|
23523
23530
|
"- All implementation happens in RLM worker sessions spawned per attempt.",
|
|
23524
23531
|
"- Strategy changes must be written to PLAN.md / RLM_INSTRUCTIONS.md so each new worker sees them.",
|
|
23525
|
-
"- Stop the loop explicitly with ralph_end_supervision() when the user is done or verification passed."
|
|
23532
|
+
"- Stop the loop explicitly with ralph_end_supervision() when the user is done or verification passed.",
|
|
23533
|
+
"",
|
|
23534
|
+
"Onboarding checklist (supervisor):",
|
|
23535
|
+
"- If setup is incomplete, run ralph_doctor(autofix=true).",
|
|
23536
|
+
"- If PLAN.md is missing or weak, run ralph_quickstart_wizard(...) or ralph_bootstrap_plan(...) + ralph_validate_plan().",
|
|
23537
|
+
"- Start supervision with ralph_create_supervisor_session(start_loop=true) unless autoStartOnMainIdle is enabled.",
|
|
23538
|
+
"- Use ralph_supervision_status() to confirm current attempt and active sessions.",
|
|
23539
|
+
"",
|
|
23540
|
+
"Protocol files (purpose):",
|
|
23541
|
+
"- PLAN.md: goals, milestones, definition of done (authoritative).",
|
|
23542
|
+
"- RLM_INSTRUCTIONS.md: inner loop operating manual (authoritative, updated between attempts).",
|
|
23543
|
+
"- CURRENT_STATE.md: scratch for the current attempt only.",
|
|
23544
|
+
"- PREVIOUS_STATE.md: snapshot of last attempt's scratch.",
|
|
23545
|
+
"- NOTES_AND_LEARNINGS.md: durable learnings across attempts.",
|
|
23546
|
+
"- CONVERSATION.md / SUPERVISOR_LOG.md: status feed and timeline for the supervisor.",
|
|
23547
|
+
"- CONTEXT_FOR_RLM.md: large reference; workers must use rlm_grep/rlm_slice."
|
|
23526
23548
|
].join(`
|
|
23527
23549
|
`),
|
|
23528
23550
|
systemPromptAppend: "",
|
|
@@ -23536,7 +23558,12 @@ var DEFAULT_TEMPLATES = {
|
|
|
23536
23558
|
"- NOTES_AND_LEARNINGS.md \u2014 append-only durable learnings",
|
|
23537
23559
|
"- CONVERSATION.md \u2014 append-only supervisor-visible status feed",
|
|
23538
23560
|
"- CONTEXT_FOR_RLM.md \u2014 large reference; access via rlm_grep + rlm_slice",
|
|
23539
|
-
"- .opencode/agents/<name>/ \u2014 sub-agent state directories"
|
|
23561
|
+
"- .opencode/agents/<name>/ \u2014 sub-agent state directories",
|
|
23562
|
+
"",
|
|
23563
|
+
"## Loop control",
|
|
23564
|
+
"- ralph_create_supervisor_session(start_loop=true) to start",
|
|
23565
|
+
"- ralph_pause_supervision() / ralph_resume_supervision() to pause/resume",
|
|
23566
|
+
"- ralph_end_supervision() to stop and clean up"
|
|
23540
23567
|
].join(`
|
|
23541
23568
|
`),
|
|
23542
23569
|
continuePrompt: [
|
|
@@ -23680,6 +23707,7 @@ var DEFAULT_TEMPLATES = {
|
|
|
23680
23707
|
"- You do NOT write code yourself; you are not the RLM worker.",
|
|
23681
23708
|
"- After reviewing state and optionally updating PLAN.md / RLM_INSTRUCTIONS.md,",
|
|
23682
23709
|
" call ralph_spawn_worker() to hand off to the RLM worker for this attempt.",
|
|
23710
|
+
"- You MUST call ralph_spawn_worker() exactly once per attempt.",
|
|
23683
23711
|
"- Then STOP. The plugin verifies independently and will spawn the next Ralph session if needed.",
|
|
23684
23712
|
"",
|
|
23685
23713
|
"Role boundaries:",
|
|
@@ -23700,7 +23728,7 @@ var DEFAULT_TEMPLATES = {
|
|
|
23700
23728
|
" guidance for the next worker based on patterns in the failures.",
|
|
23701
23729
|
"5. Optionally call ralph_set_status('running', 'strategy finalized').",
|
|
23702
23730
|
"6. Call ralph_report() summarizing strategy changes for this attempt.",
|
|
23703
|
-
"7. Call ralph_spawn_worker() to delegate the coding work to a fresh RLM worker.",
|
|
23731
|
+
"7. Call ralph_spawn_worker() to delegate the coding work to a fresh RLM worker (required).",
|
|
23704
23732
|
"8. STOP \u2014 the plugin handles verification and will spawn attempt {{nextAttempt}} if needed.",
|
|
23705
23733
|
"",
|
|
23706
23734
|
"You do not write code. Your value is strategic context adjustment between attempts.",
|
|
@@ -23708,7 +23736,10 @@ var DEFAULT_TEMPLATES = {
|
|
|
23708
23736
|
"Tool meaning:",
|
|
23709
23737
|
"- ralph_update_plan / ralph_update_rlm_instructions = durable strategy changes",
|
|
23710
23738
|
"- ralph_spawn_worker = handoff to implementation session",
|
|
23711
|
-
"- ralph_report = visible summary for the supervisor"
|
|
23739
|
+
"- ralph_report = visible summary for the supervisor",
|
|
23740
|
+
"",
|
|
23741
|
+
"Example flow:",
|
|
23742
|
+
'- ralph_load_context() \u2192 ralph_report("Strategy: update PLAN.md with constraint X") \u2192 ralph_spawn_worker() \u2192 STOP'
|
|
23712
23743
|
].join(`
|
|
23713
23744
|
`)
|
|
23714
23745
|
};
|
|
@@ -24238,6 +24269,27 @@ var RalphRLM = async ({ client, $, worktree }) => {
|
|
|
24238
24269
|
}).catch(() => {});
|
|
24239
24270
|
}
|
|
24240
24271
|
};
|
|
24272
|
+
const sendPromptWithFallback = async (sessionId, text, label, originSessionId) => {
|
|
24273
|
+
try {
|
|
24274
|
+
await client.session.promptAsync({
|
|
24275
|
+
path: { id: sessionId },
|
|
24276
|
+
body: { parts: [{ type: "text", text }] }
|
|
24277
|
+
});
|
|
24278
|
+
return true;
|
|
24279
|
+
} catch (err) {
|
|
24280
|
+
await notifySupervisor("supervisor", `${label} promptAsync failed: ${err instanceof Error ? err.message : String(err)}`, "warning", true, originSessionId);
|
|
24281
|
+
}
|
|
24282
|
+
try {
|
|
24283
|
+
await client.session.prompt({
|
|
24284
|
+
path: { id: sessionId },
|
|
24285
|
+
body: { parts: [{ type: "text", text }] }
|
|
24286
|
+
});
|
|
24287
|
+
return true;
|
|
24288
|
+
} catch (err) {
|
|
24289
|
+
await notifySupervisor("supervisor", `${label} prompt fallback failed: ${err instanceof Error ? err.message : String(err)}`, "error", true, originSessionId);
|
|
24290
|
+
return false;
|
|
24291
|
+
}
|
|
24292
|
+
};
|
|
24241
24293
|
const detectProjectDefaults = (root) => exports_Effect.gen(function* () {
|
|
24242
24294
|
const j = (f) => NodePath.join(root, f);
|
|
24243
24295
|
const hasBunLock = (yield* fileExists(j("bun.lockb"))) || (yield* fileExists(j("bun.lock")));
|
|
@@ -25134,6 +25186,10 @@ No pending questions found.`));
|
|
|
25134
25186
|
supervisor.activeReviewerSessionId = undefined;
|
|
25135
25187
|
supervisor.activeReviewerOutputPath = undefined;
|
|
25136
25188
|
await persistReviewerState();
|
|
25189
|
+
if (supervisor.ralphHandoffTimer) {
|
|
25190
|
+
clearTimeout(supervisor.ralphHandoffTimer);
|
|
25191
|
+
supervisor.ralphHandoffTimer = undefined;
|
|
25192
|
+
}
|
|
25137
25193
|
if (args2.clear_binding === true) {
|
|
25138
25194
|
supervisor.sessionId = undefined;
|
|
25139
25195
|
}
|
|
@@ -25575,7 +25631,8 @@ Set a new goal and run again.
|
|
|
25575
25631
|
const st = sessionMap.get(sessionId);
|
|
25576
25632
|
sessionMap.delete(sessionId);
|
|
25577
25633
|
let didUpdate = false;
|
|
25578
|
-
|
|
25634
|
+
const clearedRalph = supervisor.currentRalphSessionId === sessionId;
|
|
25635
|
+
if (clearedRalph) {
|
|
25579
25636
|
supervisor.currentRalphSessionId = undefined;
|
|
25580
25637
|
didUpdate = true;
|
|
25581
25638
|
}
|
|
@@ -25591,6 +25648,10 @@ Set a new goal and run again.
|
|
|
25591
25648
|
didUpdate = true;
|
|
25592
25649
|
await persistReviewerState();
|
|
25593
25650
|
}
|
|
25651
|
+
if (supervisor.ralphHandoffTimer && clearedRalph) {
|
|
25652
|
+
clearTimeout(supervisor.ralphHandoffTimer);
|
|
25653
|
+
supervisor.ralphHandoffTimer = undefined;
|
|
25654
|
+
}
|
|
25594
25655
|
if (didUpdate && st) {
|
|
25595
25656
|
await notifySupervisor(`${st.role}/attempt-${st.attempt}`, `${st.role} session ended (${reason}).`, "info", true, sessionId);
|
|
25596
25657
|
}
|
|
@@ -25651,10 +25712,18 @@ ${interpolate(templates.continuePrompt, { attempt: String(attemptN), verdict })}
|
|
|
25651
25712
|
attempt: String(attempt),
|
|
25652
25713
|
nextAttempt: String(attempt + 1)
|
|
25653
25714
|
});
|
|
25654
|
-
await
|
|
25655
|
-
|
|
25656
|
-
|
|
25657
|
-
|
|
25715
|
+
const promptOk = await sendPromptWithFallback(workerId, promptText, `Worker prompt (attempt ${attempt})`, workerId);
|
|
25716
|
+
if (!promptOk) {
|
|
25717
|
+
mutateSession(workerId, (s) => {
|
|
25718
|
+
s.reportedStatus = "error";
|
|
25719
|
+
s.reportedStatusNote = "Worker prompt failed";
|
|
25720
|
+
});
|
|
25721
|
+
supervisor.currentWorkerSessionId = undefined;
|
|
25722
|
+
await client.session.abort({ path: { id: workerId } }).catch(() => {});
|
|
25723
|
+
await notifySupervisor(`worker/attempt-${attempt}`, "Worker prompt failed; supervision paused. Retry with ralph_create_supervisor_session(restart_if_done=true).", "error", true);
|
|
25724
|
+
supervisor.paused = true;
|
|
25725
|
+
throw new Error("Worker prompt failed");
|
|
25726
|
+
}
|
|
25658
25727
|
await notifySupervisor(`supervisor/attempt-${attempt}`, `Spawned worker session ${workerId}.`, "info", true);
|
|
25659
25728
|
return workerId;
|
|
25660
25729
|
};
|
|
@@ -25667,10 +25736,16 @@ ${interpolate(templates.continuePrompt, { attempt: String(attemptN), verdict })}
|
|
|
25667
25736
|
if (st.workerSpawned) {
|
|
25668
25737
|
throw new Error("ralph_spawn_worker() has already been called for this attempt.");
|
|
25669
25738
|
}
|
|
25739
|
+
if (supervisor.ralphHandoffTimer) {
|
|
25740
|
+
clearTimeout(supervisor.ralphHandoffTimer);
|
|
25741
|
+
supervisor.ralphHandoffTimer = undefined;
|
|
25742
|
+
}
|
|
25743
|
+
const workerId = await spawnRlmWorker(st.attempt);
|
|
25670
25744
|
mutateSession(sessionID, (s) => {
|
|
25671
25745
|
s.workerSpawned = true;
|
|
25672
25746
|
});
|
|
25673
|
-
|
|
25747
|
+
supervisor.ralphHandoffRetries = 0;
|
|
25748
|
+
supervisor.ralphHandoffAttempt = st.attempt;
|
|
25674
25749
|
await notifySupervisor(`ralph/attempt-${st.attempt}`, `Delegated coding to worker session ${workerId}.`, "info", true, sessionID);
|
|
25675
25750
|
return JSON.stringify({ ok: true, workerSessionId: workerId, attempt: st.attempt }, null, 2);
|
|
25676
25751
|
};
|
|
@@ -25679,6 +25754,10 @@ ${interpolate(templates.continuePrompt, { attempt: String(attemptN), verdict })}
|
|
|
25679
25754
|
body: { title: `ralph-strategist-attempt-${attempt}` }
|
|
25680
25755
|
});
|
|
25681
25756
|
const ralphId = result.data?.id ?? `ralph-${Date.now()}`;
|
|
25757
|
+
if (supervisor.ralphHandoffAttempt !== attempt) {
|
|
25758
|
+
supervisor.ralphHandoffAttempt = attempt;
|
|
25759
|
+
supervisor.ralphHandoffRetries = 0;
|
|
25760
|
+
}
|
|
25682
25761
|
supervisor.currentRalphSessionId = ralphId;
|
|
25683
25762
|
sessionMap.set(ralphId, freshSession("ralph", attempt));
|
|
25684
25763
|
mutateSession(ralphId, (s) => {
|
|
@@ -25688,10 +25767,39 @@ ${interpolate(templates.continuePrompt, { attempt: String(attemptN), verdict })}
|
|
|
25688
25767
|
attempt: String(attempt),
|
|
25689
25768
|
nextAttempt: String(attempt + 1)
|
|
25690
25769
|
});
|
|
25691
|
-
await
|
|
25692
|
-
|
|
25693
|
-
|
|
25694
|
-
|
|
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);
|
|
25695
25803
|
await notifySupervisor(`supervisor/attempt-${attempt}`, `Spawned Ralph strategist session ${ralphId}.`, "info", true);
|
|
25696
25804
|
};
|
|
25697
25805
|
const handleWorkerIdle = async (workerSessionId) => {
|
|
@@ -25821,7 +25929,7 @@ ${interpolate(templates.continuePrompt, { attempt: String(attemptN), verdict })}
|
|
|
25821
25929
|
},
|
|
25822
25930
|
"experimental.chat.system.transform": async (input, output) => {
|
|
25823
25931
|
output.system = output.system ?? [];
|
|
25824
|
-
const sessionID = input.sessionID ?? input.session_id;
|
|
25932
|
+
const sessionID = input.sessionID ?? input.session_id ?? input.session?.id;
|
|
25825
25933
|
const role = sessionMap.get(sessionID ?? "")?.role;
|
|
25826
25934
|
const base = role === "worker" || role === "subagent" ? templates.workerSystemPrompt : role === "ralph" ? templates.ralphSessionSystemPrompt : templates.systemPrompt;
|
|
25827
25935
|
const full = templates.systemPromptAppend ? `${base}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-ralph-rlm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
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",
|