opencode-ralph-rlm 0.1.8 → 0.1.9

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
@@ -221,7 +221,8 @@ Create `.opencode/ralph.json`. All fields are optional — the plugin runs with
221
221
  | `statusVerbosity` | `"normal"` | Supervisor status emission level: `minimal` (warnings/errors), `normal`, or `verbose`. |
222
222
  | `maxAttempts` | `20` | Hard stop after this many failed verify attempts. |
223
223
  | `heartbeatMinutes` | `15` | Warn if active strategist/worker has no progress for this many minutes. |
224
- | `verify.command` | | Shell command to run as an array, e.g. `["bun", "run", "verify"]`. If omitted, verify always returns `unknown`. |
224
+ | `verifyTimeoutMinutes` | `0` | Timeout for verify command in minutes. `0` disables timeouts. |
225
+ | `verify.command` | - | Shell command to run as an array, e.g. `["bun", "run", "verify"]`. If omitted, verify always returns `unknown`. |
225
226
  | `verify.cwd` | `"."` | Working directory for the verify command, relative to the repo root. |
226
227
  | `gateDestructiveToolsUntilContextLoaded` | `true` | Block `write`, `edit`, `bash`, etc. until `ralph_load_context()` has been called in the current attempt. |
227
228
  | `maxRlmSliceLines` | `200` | Maximum lines a single `rlm_slice` call may return. |
@@ -439,6 +440,8 @@ Stop supervision for the current process. This prevents further auto-loop orches
439
440
 
440
441
  - Use this when you want to pause/stop Ralph from spawning more sessions.
441
442
  - Use this after verification passes and the user confirms they are done, or when the user asks to stop the loop.
443
+ - Active child sessions are aborted and any running verify command is stopped.
444
+ - Pass `delete_sessions: true` to delete child sessions after aborting them.
442
445
  - Resume later with `ralph_create_supervisor_session(restart_if_done=true)`.
443
446
 
444
447
  #### `ralph_supervision_status()`
package/dist/ralph-rlm.js CHANGED
@@ -23417,6 +23417,7 @@ 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
+ verifyTimeoutMinutes: exports_Schema.optional(exports_Schema.Number),
23420
23421
  verify: exports_Schema.optional(VerifyConfigSchema),
23421
23422
  gateDestructiveToolsUntilContextLoaded: exports_Schema.optional(exports_Schema.Boolean),
23422
23423
  maxRlmSliceLines: exports_Schema.optional(exports_Schema.Number),
@@ -23455,6 +23456,7 @@ var CONFIG_DEFAULTS = {
23455
23456
  statusVerbosity: "normal",
23456
23457
  maxAttempts: 20,
23457
23458
  heartbeatMinutes: 15,
23459
+ verifyTimeoutMinutes: 0,
23458
23460
  gateDestructiveToolsUntilContextLoaded: true,
23459
23461
  maxRlmSliceLines: 200,
23460
23462
  requireGrepBeforeLargeSlice: true,
@@ -23480,6 +23482,7 @@ function resolveConfig(raw) {
23480
23482
  statusVerbosity: raw.statusVerbosity ?? CONFIG_DEFAULTS.statusVerbosity,
23481
23483
  maxAttempts: toBoundedInt(raw.maxAttempts, CONFIG_DEFAULTS.maxAttempts, 1, 500),
23482
23484
  heartbeatMinutes: toBoundedInt(raw.heartbeatMinutes, CONFIG_DEFAULTS.heartbeatMinutes, 1, 240),
23485
+ verifyTimeoutMinutes: toBoundedInt(raw.verifyTimeoutMinutes, CONFIG_DEFAULTS.verifyTimeoutMinutes, 0, 240),
23483
23486
  ...verify !== undefined ? { verify } : {},
23484
23487
  gateDestructiveToolsUntilContextLoaded: raw.gateDestructiveToolsUntilContextLoaded ?? CONFIG_DEFAULTS.gateDestructiveToolsUntilContextLoaded,
23485
23488
  maxRlmSliceLines,
@@ -23762,7 +23765,26 @@ var writePendingInput = async (root, data) => {
23762
23765
  await NodeFs.writeFile(p, JSON.stringify(data, null, 2), "utf8");
23763
23766
  };
23764
23767
  var sleep5 = (ms) => new Promise((r) => setTimeout(r, ms));
23765
- async function runCommand(command, cwd) {
23768
+ var activeCommands = new Set;
23769
+ var stopCommand = (cmd, reason) => {
23770
+ if (cmd.child.killed)
23771
+ return;
23772
+ cmd.timedOut = cmd.timedOut || reason === "timeout";
23773
+ try {
23774
+ cmd.child.kill();
23775
+ } catch {}
23776
+ cmd.killTimer = setTimeout(() => {
23777
+ try {
23778
+ cmd.child.kill("SIGKILL");
23779
+ } catch {}
23780
+ }, 2000);
23781
+ };
23782
+ var stopAllCommands = (reason) => {
23783
+ for (const cmd of activeCommands) {
23784
+ stopCommand(cmd, reason);
23785
+ }
23786
+ };
23787
+ async function runCommand(command, cwd, options) {
23766
23788
  return await new Promise((resolve) => {
23767
23789
  const child = spawn(command[0] ?? "", command.slice(1), {
23768
23790
  cwd,
@@ -23772,6 +23794,18 @@ async function runCommand(command, cwd) {
23772
23794
  });
23773
23795
  let stdout = "";
23774
23796
  let stderr = "";
23797
+ const entry = {
23798
+ child,
23799
+ label: options?.label ?? command.join(" "),
23800
+ startedAt: Date.now(),
23801
+ timedOut: false
23802
+ };
23803
+ activeCommands.add(entry);
23804
+ if (options?.timeoutMs && options.timeoutMs > 0) {
23805
+ entry.timeoutTimer = setTimeout(() => {
23806
+ stopCommand(entry, "timeout");
23807
+ }, options.timeoutMs);
23808
+ }
23775
23809
  child.stdout?.on("data", (chunk3) => {
23776
23810
  stdout += String(chunk3);
23777
23811
  });
@@ -23779,11 +23813,23 @@ async function runCommand(command, cwd) {
23779
23813
  stderr += String(chunk3);
23780
23814
  });
23781
23815
  child.on("error", (err) => {
23816
+ if (entry.timeoutTimer)
23817
+ clearTimeout(entry.timeoutTimer);
23818
+ if (entry.killTimer)
23819
+ clearTimeout(entry.killTimer);
23820
+ activeCommands.delete(entry);
23782
23821
  resolve({ ok: false, code: null, stdout, stderr: `${stderr}
23783
23822
  ${String(err)}`.trim() });
23784
23823
  });
23785
23824
  child.on("close", (code) => {
23786
- resolve({ ok: code === 0, code, stdout, stderr });
23825
+ if (entry.timeoutTimer)
23826
+ clearTimeout(entry.timeoutTimer);
23827
+ if (entry.killTimer)
23828
+ clearTimeout(entry.killTimer);
23829
+ activeCommands.delete(entry);
23830
+ const timeoutNote = entry.timedOut ? `
23831
+ [ralph] command timed out` : "";
23832
+ resolve({ ok: code === 0 && !entry.timedOut, code, stdout, stderr: `${stderr}${timeoutNote}`.trim() });
23787
23833
  });
23788
23834
  });
23789
23835
  }
@@ -24295,9 +24341,11 @@ var RalphRLM = async ({ client, $, worktree }) => {
24295
24341
  }
24296
24342
  const verifyCmd = cfg.verify.command;
24297
24343
  const cwd = NodePath.join(root, cfg.verify.cwd ?? ".");
24344
+ const timeoutMs = cfg.verifyTimeoutMinutes > 0 ? cfg.verifyTimeoutMinutes * 60000 : null;
24298
24345
  return yield* exports_Effect.tryPromise({
24299
24346
  try: async () => {
24300
- const result = await runCommand(verifyCmd, cwd);
24347
+ const options = timeoutMs ? { timeoutMs, label: "verify" } : { label: "verify" };
24348
+ const result = await runCommand(verifyCmd, cwd, options);
24301
24349
  if (result.ok) {
24302
24350
  return JSON.stringify({ verdict: "pass", output: result.stdout }, null, 2);
24303
24351
  }
@@ -25024,6 +25072,7 @@ No pending questions found.`));
25024
25072
  description: "Stop Ralph supervision for this process. Prevents further auto-loop orchestration until restarted.",
25025
25073
  args: {
25026
25074
  reason: tool.schema.string().optional().describe("Optional reason for ending supervision."),
25075
+ delete_sessions: tool.schema.boolean().optional().describe("Also delete child sessions after aborting them (default false)."),
25027
25076
  clear_binding: tool.schema.boolean().optional().describe("Clear supervisor session binding after stop (default false).")
25028
25077
  },
25029
25078
  async execute(args2, ctx) {
@@ -25031,6 +25080,15 @@ No pending questions found.`));
25031
25080
  const reason = args2.reason?.trim();
25032
25081
  supervisor.done = true;
25033
25082
  supervisor.paused = true;
25083
+ const sessionsToAbort = Array.from(sessionMap.keys());
25084
+ for (const id of sessionsToAbort) {
25085
+ await client.session.abort({ path: { id } }).catch(() => {});
25086
+ if (args2.delete_sessions) {
25087
+ await client.session.delete({ path: { id } }).catch(() => {});
25088
+ }
25089
+ }
25090
+ stopAllCommands("supervision-ended");
25091
+ sessionMap.clear();
25034
25092
  supervisor.currentRalphSessionId = undefined;
25035
25093
  supervisor.currentWorkerSessionId = undefined;
25036
25094
  supervisor.activeReviewerName = undefined;
@@ -25451,6 +25509,8 @@ Set a new goal and run again.
25451
25509
  }
25452
25510
  });
25453
25511
  const emitHeartbeatWarnings = async () => {
25512
+ if (supervisor.done || supervisor.paused)
25513
+ return;
25454
25514
  const cfg = await run(getConfig());
25455
25515
  const thresholdMs = cfg.heartbeatMinutes * 60000;
25456
25516
  const now2 = Date.now();
@@ -25473,6 +25533,30 @@ Set a new goal and run again.
25473
25533
  await maybeWarn(supervisor.currentRalphSessionId, "Strategist");
25474
25534
  await maybeWarn(supervisor.currentWorkerSessionId, "Worker");
25475
25535
  };
25536
+ const clearSessionTracking = async (sessionId, reason) => {
25537
+ const st = sessionMap.get(sessionId);
25538
+ sessionMap.delete(sessionId);
25539
+ let didUpdate = false;
25540
+ if (supervisor.currentRalphSessionId === sessionId) {
25541
+ supervisor.currentRalphSessionId = undefined;
25542
+ didUpdate = true;
25543
+ }
25544
+ if (supervisor.currentWorkerSessionId === sessionId) {
25545
+ supervisor.currentWorkerSessionId = undefined;
25546
+ didUpdate = true;
25547
+ }
25548
+ if (supervisor.activeReviewerSessionId === sessionId) {
25549
+ supervisor.activeReviewerSessionId = undefined;
25550
+ supervisor.activeReviewerName = undefined;
25551
+ supervisor.activeReviewerAttempt = undefined;
25552
+ supervisor.activeReviewerOutputPath = undefined;
25553
+ didUpdate = true;
25554
+ await persistReviewerState();
25555
+ }
25556
+ if (didUpdate && st) {
25557
+ await notifySupervisor(`${st.role}/attempt-${st.attempt}`, `${st.role} session ended (${reason}).`, "info", true, sessionId);
25558
+ }
25559
+ };
25476
25560
  const runAndParseVerify = async () => {
25477
25561
  const raw = await run(runVerify(worktree));
25478
25562
  try {
@@ -25740,7 +25824,13 @@ ${templates.systemPromptAppend}` : base;
25740
25824
  getSession(sessionID);
25741
25825
  }
25742
25826
  if (event?.type === "session.status" && sessionID) {
25827
+ if (supervisor.done || supervisor.paused)
25828
+ return;
25743
25829
  const state = sessionMap.get(sessionID);
25830
+ if (state?.role === "worker" && supervisor.currentWorkerSessionId !== sessionID)
25831
+ return;
25832
+ if (state?.role === "ralph" && supervisor.currentRalphSessionId !== sessionID)
25833
+ return;
25744
25834
  if (state) {
25745
25835
  mutateSession(sessionID, (s) => {
25746
25836
  s.lastProgressAt = Date.now();
@@ -25752,6 +25842,8 @@ ${templates.systemPromptAppend}` : base;
25752
25842
  }
25753
25843
  }
25754
25844
  if (event?.type === "session.idle" && sessionID) {
25845
+ if (supervisor.done || supervisor.paused)
25846
+ return;
25755
25847
  await emitHeartbeatWarnings().catch((err) => {
25756
25848
  appLog("error", "heartbeat warning error", { error: String(err) });
25757
25849
  });
@@ -25769,6 +25861,9 @@ ${templates.systemPromptAppend}` : base;
25769
25861
  });
25770
25862
  }
25771
25863
  }
25864
+ if (sessionID && (event?.type === "session.closed" || event?.type === "session.ended" || event?.type === "session.deleted")) {
25865
+ await clearSessionTracking(sessionID, event.type);
25866
+ }
25772
25867
  }
25773
25868
  };
25774
25869
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-ralph-rlm",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
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",