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 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
- if (supervisor.currentRalphSessionId === sessionId) {
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 client.session.promptAsync({
25655
- path: { id: workerId },
25656
- body: { parts: [{ type: "text", text: promptText }] }
25657
- }).catch(() => {});
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
- const workerId = await spawnRlmWorker(st.attempt);
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 client.session.promptAsync({
25692
- path: { id: ralphId },
25693
- body: { parts: [{ type: "text", text: promptText }] }
25694
- }).catch(() => {});
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.11",
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",