opencode-ralph-rlm 0.1.7 → 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 +15 -1
- package/dist/ralph-rlm.js +187 -8
- package/package.json +1 -1
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
|
-
| `
|
|
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. |
|
|
@@ -438,6 +439,9 @@ Bind the current session as the supervisor and optionally start attempt 1 immedi
|
|
|
438
439
|
Stop supervision for the current process. This prevents further auto-loop orchestration until restarted.
|
|
439
440
|
|
|
440
441
|
- Use this when you want to pause/stop Ralph from spawning more sessions.
|
|
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.
|
|
441
445
|
- Resume later with `ralph_create_supervisor_session(restart_if_done=true)`.
|
|
442
446
|
|
|
443
447
|
#### `ralph_supervision_status()`
|
|
@@ -572,6 +576,16 @@ args:
|
|
|
572
576
|
post_to_conversation boolean optional Post to main conversation (default: true)
|
|
573
577
|
```
|
|
574
578
|
|
|
579
|
+
#### `ralph_peek_worker(maxLines?, post_to_conversation?)`
|
|
580
|
+
|
|
581
|
+
Snapshot the active worker's `CURRENT_STATE.md` and optionally post it into the main conversation for quick "peek" access in the TUI.
|
|
582
|
+
|
|
583
|
+
```
|
|
584
|
+
args:
|
|
585
|
+
maxLines number optional Max lines to include (default: 120)
|
|
586
|
+
post_to_conversation boolean optional Post to main conversation (default: true)
|
|
587
|
+
```
|
|
588
|
+
|
|
575
589
|
#### `ralph_ask(question, context?, timeout_minutes?)`
|
|
576
590
|
|
|
577
591
|
Ask a question and **block** until you respond via `ralph_respond()`. The question is written to `.opencode/pending_input.json`, a toast appears in the main session, and the main conversation is prompted with the question ID and response instruction. The calling session polls every 5 seconds.
|
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,
|
|
@@ -23509,6 +23512,7 @@ var DEFAULT_TEMPLATES = {
|
|
|
23509
23512
|
" When you receive one, call ralph_respond(id, answer) to unblock the session.",
|
|
23510
23513
|
"- Use ralph_doctor() to check setup, ralph_bootstrap_plan() to generate PLAN/TODOS,",
|
|
23511
23514
|
" ralph_create_supervisor_session() to bind/start explicitly, ralph_pause_supervision()/ralph_resume_supervision() to control execution, and ralph_end_supervision() to stop.",
|
|
23515
|
+
"- End supervision when verification has passed and the user confirms they are done, or when the user explicitly asks to stop the loop.",
|
|
23512
23516
|
"- Optional reviewer flow: worker marks readiness with ralph_request_review(); supervisor runs ralph_run_reviewer().",
|
|
23513
23517
|
"- Monitor progress in SUPERVISOR_LOG.md, CONVERSATION.md, or via toast notifications."
|
|
23514
23518
|
].join(`
|
|
@@ -23761,7 +23765,26 @@ var writePendingInput = async (root, data) => {
|
|
|
23761
23765
|
await NodeFs.writeFile(p, JSON.stringify(data, null, 2), "utf8");
|
|
23762
23766
|
};
|
|
23763
23767
|
var sleep5 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
23764
|
-
|
|
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) {
|
|
23765
23788
|
return await new Promise((resolve) => {
|
|
23766
23789
|
const child = spawn(command[0] ?? "", command.slice(1), {
|
|
23767
23790
|
cwd,
|
|
@@ -23771,6 +23794,18 @@ async function runCommand(command, cwd) {
|
|
|
23771
23794
|
});
|
|
23772
23795
|
let stdout = "";
|
|
23773
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
|
+
}
|
|
23774
23809
|
child.stdout?.on("data", (chunk3) => {
|
|
23775
23810
|
stdout += String(chunk3);
|
|
23776
23811
|
});
|
|
@@ -23778,11 +23813,23 @@ async function runCommand(command, cwd) {
|
|
|
23778
23813
|
stderr += String(chunk3);
|
|
23779
23814
|
});
|
|
23780
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);
|
|
23781
23821
|
resolve({ ok: false, code: null, stdout, stderr: `${stderr}
|
|
23782
23822
|
${String(err)}`.trim() });
|
|
23783
23823
|
});
|
|
23784
23824
|
child.on("close", (code) => {
|
|
23785
|
-
|
|
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() });
|
|
23786
23833
|
});
|
|
23787
23834
|
});
|
|
23788
23835
|
}
|
|
@@ -24102,7 +24149,21 @@ var RalphRLM = async ({ client, $, worktree }) => {
|
|
|
24102
24149
|
- ${nowISO()} rotated
|
|
24103
24150
|
`)).catch(() => {});
|
|
24104
24151
|
};
|
|
24152
|
+
const DEDUPE_WINDOW_MS = 5000;
|
|
24153
|
+
const recentNotices = new Map;
|
|
24154
|
+
const shouldDedupe = (key) => {
|
|
24155
|
+
const now2 = Date.now();
|
|
24156
|
+
const last3 = recentNotices.get(key);
|
|
24157
|
+
if (last3 && now2 - last3 < DEDUPE_WINDOW_MS)
|
|
24158
|
+
return true;
|
|
24159
|
+
if (recentNotices.size > 200)
|
|
24160
|
+
recentNotices.clear();
|
|
24161
|
+
recentNotices.set(key, now2);
|
|
24162
|
+
return false;
|
|
24163
|
+
};
|
|
24105
24164
|
const appendConversationEntry = async (source, message) => {
|
|
24165
|
+
if (shouldDedupe(`conv|${source}|${message}`))
|
|
24166
|
+
return;
|
|
24106
24167
|
const cfg = await run(getConfig());
|
|
24107
24168
|
await rotateConversationLogIfNeeded(cfg);
|
|
24108
24169
|
const ts = nowISO();
|
|
@@ -24113,6 +24174,8 @@ var RalphRLM = async ({ client, $, worktree }) => {
|
|
|
24113
24174
|
});
|
|
24114
24175
|
};
|
|
24115
24176
|
const notifySupervisor = async (source, message, level = "info", postToConversation = true, originSessionId) => {
|
|
24177
|
+
if (shouldDedupe(`sup|${source}|${level}|${message}`))
|
|
24178
|
+
return;
|
|
24116
24179
|
const cfg = await run(getConfig());
|
|
24117
24180
|
if (!messagePassesVerbosity(cfg.statusVerbosity, level))
|
|
24118
24181
|
return;
|
|
@@ -24137,6 +24200,34 @@ var RalphRLM = async ({ client, $, worktree }) => {
|
|
|
24137
24200
|
}).catch(() => {});
|
|
24138
24201
|
}
|
|
24139
24202
|
};
|
|
24203
|
+
const detectProjectDefaults = (root) => exports_Effect.gen(function* () {
|
|
24204
|
+
const j = (f) => NodePath.join(root, f);
|
|
24205
|
+
const hasBunLock = (yield* fileExists(j("bun.lockb"))) || (yield* fileExists(j("bun.lock")));
|
|
24206
|
+
if (hasBunLock)
|
|
24207
|
+
return { verify: ["bun", "run", "verify"], install: "bun install" };
|
|
24208
|
+
const hasYarnLock = yield* fileExists(j("yarn.lock"));
|
|
24209
|
+
if (hasYarnLock)
|
|
24210
|
+
return { verify: ["yarn", "test"], install: "yarn install" };
|
|
24211
|
+
const hasPnpmLock = yield* fileExists(j("pnpm-lock.yaml"));
|
|
24212
|
+
if (hasPnpmLock)
|
|
24213
|
+
return { verify: ["pnpm", "test"], install: "pnpm install" };
|
|
24214
|
+
const hasPkg = yield* fileExists(j("package.json"));
|
|
24215
|
+
if (hasPkg)
|
|
24216
|
+
return { verify: ["npm", "test"], install: "npm install" };
|
|
24217
|
+
const hasCargo = yield* fileExists(j("Cargo.toml"));
|
|
24218
|
+
if (hasCargo)
|
|
24219
|
+
return { verify: ["cargo", "test"], install: "cargo build" };
|
|
24220
|
+
const hasPy = yield* fileExists(j("pyproject.toml"));
|
|
24221
|
+
const hasReq = yield* fileExists(j("requirements.txt"));
|
|
24222
|
+
if (hasReq)
|
|
24223
|
+
return { verify: ["python", "-m", "pytest"], install: "pip install -r requirements.txt" };
|
|
24224
|
+
if (hasPy)
|
|
24225
|
+
return { verify: ["python", "-m", "pytest"], install: "pip install ." };
|
|
24226
|
+
const hasMake = yield* fileExists(j("Makefile"));
|
|
24227
|
+
if (hasMake)
|
|
24228
|
+
return { verify: ["make", "test"], install: "make" };
|
|
24229
|
+
return { verify: ["bun", "run", "verify"], install: "bun install" };
|
|
24230
|
+
});
|
|
24140
24231
|
const checkSetup = async (root, cfg) => {
|
|
24141
24232
|
const diagnostics = {
|
|
24142
24233
|
ready: true,
|
|
@@ -24148,7 +24239,8 @@ var RalphRLM = async ({ client, $, worktree }) => {
|
|
|
24148
24239
|
if (!cfg.verify || cfg.verify.command.length === 0) {
|
|
24149
24240
|
diagnostics.ready = false;
|
|
24150
24241
|
diagnostics.issues.push("Missing verify.command in .opencode/ralph.json.");
|
|
24151
|
-
|
|
24242
|
+
const defaults = await run(detectProjectDefaults(root));
|
|
24243
|
+
diagnostics.suggestions.push(`Set verify.command, e.g. ${JSON.stringify(defaults.verify)}.`);
|
|
24152
24244
|
}
|
|
24153
24245
|
const planExists = await run(fileExists(j(FILES.PLAN)));
|
|
24154
24246
|
if (!planExists) {
|
|
@@ -24249,9 +24341,11 @@ var RalphRLM = async ({ client, $, worktree }) => {
|
|
|
24249
24341
|
}
|
|
24250
24342
|
const verifyCmd = cfg.verify.command;
|
|
24251
24343
|
const cwd = NodePath.join(root, cfg.verify.cwd ?? ".");
|
|
24344
|
+
const timeoutMs = cfg.verifyTimeoutMinutes > 0 ? cfg.verifyTimeoutMinutes * 60000 : null;
|
|
24252
24345
|
return yield* exports_Effect.tryPromise({
|
|
24253
24346
|
try: async () => {
|
|
24254
|
-
const
|
|
24347
|
+
const options = timeoutMs ? { timeoutMs, label: "verify" } : { label: "verify" };
|
|
24348
|
+
const result = await runCommand(verifyCmd, cwd, options);
|
|
24255
24349
|
if (result.ok) {
|
|
24256
24350
|
return JSON.stringify({ verdict: "pass", output: result.stdout }, null, 2);
|
|
24257
24351
|
}
|
|
@@ -24502,6 +24596,41 @@ ${args2.nextStep}
|
|
|
24502
24596
|
return run(runVerify(root));
|
|
24503
24597
|
}
|
|
24504
24598
|
});
|
|
24599
|
+
const tool_ralph_peek_worker = tool({
|
|
24600
|
+
description: "Snapshot the active RLM worker's CURRENT_STATE.md and optionally post it into the main conversation.",
|
|
24601
|
+
args: {
|
|
24602
|
+
maxLines: tool.schema.number().int().min(20).max(400).optional(),
|
|
24603
|
+
post_to_conversation: tool.schema.boolean().optional().describe("Whether to post the peek into the main conversation (default: true).")
|
|
24604
|
+
},
|
|
24605
|
+
async execute(args2, ctx) {
|
|
24606
|
+
const root = ctx.worktree ?? worktree;
|
|
24607
|
+
const currPath = NodePath.join(root, FILES.CURR);
|
|
24608
|
+
const ok = await run(fileExists(currPath));
|
|
24609
|
+
if (!ok)
|
|
24610
|
+
return JSON.stringify({ ok: false, missing: FILES.CURR }, null, 2);
|
|
24611
|
+
const raw = await run(readFile(currPath).pipe(exports_Effect.orElseSucceed(() => "")));
|
|
24612
|
+
const text = clampLines(raw, args2.maxLines ?? 120);
|
|
24613
|
+
const attempt = supervisor.attempt;
|
|
24614
|
+
const workerId = supervisor.currentWorkerSessionId;
|
|
24615
|
+
const header = `Worker peek${attempt ? ` (attempt ${attempt})` : ""}${workerId ? ` \u2014 ${workerId}` : ""}`;
|
|
24616
|
+
await notifySupervisor("peek", header, "info", false, ctx.sessionID);
|
|
24617
|
+
const postToConv = args2.post_to_conversation !== false;
|
|
24618
|
+
if (postToConv && supervisor.sessionId && supervisor.sessionId !== ctx.sessionID) {
|
|
24619
|
+
await client.session.promptAsync({
|
|
24620
|
+
path: { id: supervisor.sessionId },
|
|
24621
|
+
body: { parts: [{ type: "text", text: `[peek] ${header}
|
|
24622
|
+
|
|
24623
|
+
${text}` }] }
|
|
24624
|
+
}).catch(() => {});
|
|
24625
|
+
}
|
|
24626
|
+
return JSON.stringify({
|
|
24627
|
+
ok: true,
|
|
24628
|
+
attempt: attempt || null,
|
|
24629
|
+
workerSessionId: workerId ?? null,
|
|
24630
|
+
text
|
|
24631
|
+
}, null, 2);
|
|
24632
|
+
}
|
|
24633
|
+
});
|
|
24505
24634
|
const tool_subagent_peek = tool({
|
|
24506
24635
|
description: "Read file-first state from a sub-agent directory (.opencode/agents/<name>/).",
|
|
24507
24636
|
args: {
|
|
@@ -24777,6 +24906,7 @@ No pending questions found.`));
|
|
|
24777
24906
|
const diagnosticsBefore = await checkSetup(root, cfg);
|
|
24778
24907
|
const actions = [];
|
|
24779
24908
|
if (args2.autofix) {
|
|
24909
|
+
const defaults = await run(detectProjectDefaults(root));
|
|
24780
24910
|
const configPath = NodePath.join(root, ".opencode", "ralph.json");
|
|
24781
24911
|
const configExists = await run(fileExists(configPath));
|
|
24782
24912
|
if (!configExists) {
|
|
@@ -24786,7 +24916,7 @@ No pending questions found.`));
|
|
|
24786
24916
|
statusVerbosity: "normal",
|
|
24787
24917
|
maxAttempts: 25,
|
|
24788
24918
|
heartbeatMinutes: 15,
|
|
24789
|
-
verify: { command:
|
|
24919
|
+
verify: { command: defaults.verify, cwd: "." },
|
|
24790
24920
|
gateDestructiveToolsUntilContextLoaded: true,
|
|
24791
24921
|
maxRlmSliceLines: 200,
|
|
24792
24922
|
requireGrepBeforeLargeSlice: true,
|
|
@@ -24814,8 +24944,8 @@ No pending questions found.`));
|
|
|
24814
24944
|
"# Project Agent Rules",
|
|
24815
24945
|
"",
|
|
24816
24946
|
"## Build and verify",
|
|
24817
|
-
|
|
24818
|
-
|
|
24947
|
+
`- Install: ${defaults.install}`,
|
|
24948
|
+
`- Verify: ${defaults.verify.join(" ")}`,
|
|
24819
24949
|
"",
|
|
24820
24950
|
"## Loop note",
|
|
24821
24951
|
"- This project uses ralph-rlm.",
|
|
@@ -24942,6 +25072,7 @@ No pending questions found.`));
|
|
|
24942
25072
|
description: "Stop Ralph supervision for this process. Prevents further auto-loop orchestration until restarted.",
|
|
24943
25073
|
args: {
|
|
24944
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)."),
|
|
24945
25076
|
clear_binding: tool.schema.boolean().optional().describe("Clear supervisor session binding after stop (default false).")
|
|
24946
25077
|
},
|
|
24947
25078
|
async execute(args2, ctx) {
|
|
@@ -24949,6 +25080,15 @@ No pending questions found.`));
|
|
|
24949
25080
|
const reason = args2.reason?.trim();
|
|
24950
25081
|
supervisor.done = true;
|
|
24951
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();
|
|
24952
25092
|
supervisor.currentRalphSessionId = undefined;
|
|
24953
25093
|
supervisor.currentWorkerSessionId = undefined;
|
|
24954
25094
|
supervisor.activeReviewerName = undefined;
|
|
@@ -25149,13 +25289,14 @@ Set a new goal and run again.
|
|
|
25149
25289
|
const configPath = NodePath.join(root, ".opencode", "ralph.json");
|
|
25150
25290
|
const configExists = await run(fileExists(configPath));
|
|
25151
25291
|
if (!configExists) {
|
|
25292
|
+
const defaults = await run(detectProjectDefaults(root));
|
|
25152
25293
|
const defaultCfg = {
|
|
25153
25294
|
enabled: true,
|
|
25154
25295
|
autoStartOnMainIdle: false,
|
|
25155
25296
|
statusVerbosity: "normal",
|
|
25156
25297
|
maxAttempts: 25,
|
|
25157
25298
|
heartbeatMinutes: 15,
|
|
25158
|
-
verify: { command:
|
|
25299
|
+
verify: { command: defaults.verify, cwd: "." },
|
|
25159
25300
|
gateDestructiveToolsUntilContextLoaded: true,
|
|
25160
25301
|
maxRlmSliceLines: 200,
|
|
25161
25302
|
requireGrepBeforeLargeSlice: true,
|
|
@@ -25368,6 +25509,8 @@ Set a new goal and run again.
|
|
|
25368
25509
|
}
|
|
25369
25510
|
});
|
|
25370
25511
|
const emitHeartbeatWarnings = async () => {
|
|
25512
|
+
if (supervisor.done || supervisor.paused)
|
|
25513
|
+
return;
|
|
25371
25514
|
const cfg = await run(getConfig());
|
|
25372
25515
|
const thresholdMs = cfg.heartbeatMinutes * 60000;
|
|
25373
25516
|
const now2 = Date.now();
|
|
@@ -25390,6 +25533,30 @@ Set a new goal and run again.
|
|
|
25390
25533
|
await maybeWarn(supervisor.currentRalphSessionId, "Strategist");
|
|
25391
25534
|
await maybeWarn(supervisor.currentWorkerSessionId, "Worker");
|
|
25392
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
|
+
};
|
|
25393
25560
|
const runAndParseVerify = async () => {
|
|
25394
25561
|
const raw = await run(runVerify(worktree));
|
|
25395
25562
|
try {
|
|
@@ -25592,6 +25759,7 @@ ${interpolate(templates.continuePrompt, { attempt: String(attemptN), verdict })}
|
|
|
25592
25759
|
ralph_update_rlm_instructions: tool_ralph_update_rlm_instructions,
|
|
25593
25760
|
ralph_rollover: tool_ralph_rollover,
|
|
25594
25761
|
ralph_verify: tool_ralph_verify,
|
|
25762
|
+
ralph_peek_worker: tool_ralph_peek_worker,
|
|
25595
25763
|
ralph_spawn_worker: tool_ralph_spawn_worker,
|
|
25596
25764
|
subagent_peek: tool_subagent_peek,
|
|
25597
25765
|
subagent_spawn: tool_subagent_spawn,
|
|
@@ -25656,7 +25824,13 @@ ${templates.systemPromptAppend}` : base;
|
|
|
25656
25824
|
getSession(sessionID);
|
|
25657
25825
|
}
|
|
25658
25826
|
if (event?.type === "session.status" && sessionID) {
|
|
25827
|
+
if (supervisor.done || supervisor.paused)
|
|
25828
|
+
return;
|
|
25659
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;
|
|
25660
25834
|
if (state) {
|
|
25661
25835
|
mutateSession(sessionID, (s) => {
|
|
25662
25836
|
s.lastProgressAt = Date.now();
|
|
@@ -25668,6 +25842,8 @@ ${templates.systemPromptAppend}` : base;
|
|
|
25668
25842
|
}
|
|
25669
25843
|
}
|
|
25670
25844
|
if (event?.type === "session.idle" && sessionID) {
|
|
25845
|
+
if (supervisor.done || supervisor.paused)
|
|
25846
|
+
return;
|
|
25671
25847
|
await emitHeartbeatWarnings().catch((err) => {
|
|
25672
25848
|
appLog("error", "heartbeat warning error", { error: String(err) });
|
|
25673
25849
|
});
|
|
@@ -25685,6 +25861,9 @@ ${templates.systemPromptAppend}` : base;
|
|
|
25685
25861
|
});
|
|
25686
25862
|
}
|
|
25687
25863
|
}
|
|
25864
|
+
if (sessionID && (event?.type === "session.closed" || event?.type === "session.ended" || event?.type === "session.deleted")) {
|
|
25865
|
+
await clearSessionTracking(sessionID, event.type);
|
|
25866
|
+
}
|
|
25688
25867
|
}
|
|
25689
25868
|
};
|
|
25690
25869
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-ralph-rlm",
|
|
3
|
-
"version": "0.1.
|
|
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",
|