bmalph 2.9.0 → 2.10.0
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 +7 -7
- package/dist/cli.js +2 -1
- package/dist/commands/run.js +28 -15
- package/dist/installer/template-files.js +29 -0
- package/dist/run/ralph-process.js +8 -3
- package/dist/run/run-dashboard.js +4 -4
- package/package.json +1 -1
- package/ralph/drivers/DRIVER_INTERFACE.md +422 -0
- package/ralph/drivers/codex.sh +2 -2
- package/ralph/lib/response_analyzer.sh +87 -87
- package/ralph/ralph_loop.sh +61 -10
- package/ralph/templates/ralphrc.template +8 -3
package/README.md
CHANGED
|
@@ -279,12 +279,12 @@ BMAD (add Epic 2) → bmalph implement → Ralph sees changes + picks up Epic 2
|
|
|
279
279
|
|
|
280
280
|
### run options
|
|
281
281
|
|
|
282
|
-
| Flag
|
|
283
|
-
|
|
|
284
|
-
| `--driver <platform>`
|
|
285
|
-
| `--review
|
|
286
|
-
| `--interval <ms>`
|
|
287
|
-
| `--no-dashboard`
|
|
282
|
+
| Flag | Description |
|
|
283
|
+
| --------------------- | ---------------------------------------------------------------------------------------- |
|
|
284
|
+
| `--driver <platform>` | Override platform driver (claude-code, codex, opencode, copilot, cursor) |
|
|
285
|
+
| `--review [mode]` | Quality review: `enhanced` (every 5 loops) or `ultimate` (every story). Claude Code only |
|
|
286
|
+
| `--interval <ms>` | Dashboard refresh interval in milliseconds (default: 2000) |
|
|
287
|
+
| `--no-dashboard` | Run Ralph without the dashboard overlay |
|
|
288
288
|
|
|
289
289
|
### watch options
|
|
290
290
|
|
|
@@ -416,7 +416,7 @@ Safety mechanisms:
|
|
|
416
416
|
|
|
417
417
|
- **Circuit breaker** — prevents infinite loops on failing stories
|
|
418
418
|
- **Response analyzer** — detects stuck or repeating outputs
|
|
419
|
-
- **Code review** — optional
|
|
419
|
+
- **Code review** — optional quality review (`--review [mode]`, Claude Code only). Enhanced: periodic review every 5 loops. Ultimate: review after every completed story. A read-only session analyzes git diffs and feeds structured findings into the next implementation loop
|
|
420
420
|
- **Completion** — loop exits when all `@fix_plan.md` items are checked off
|
|
421
421
|
|
|
422
422
|
Cursor-specific runtime checks:
|
package/dist/cli.js
CHANGED
|
@@ -105,7 +105,8 @@ program
|
|
|
105
105
|
.option("--driver <platform>", "Override platform driver (claude-code, codex, opencode, copilot, cursor)")
|
|
106
106
|
.option("--interval <ms>", "Dashboard refresh interval in milliseconds (default: 2000)")
|
|
107
107
|
.option("--no-dashboard", "Run Ralph without the dashboard overlay")
|
|
108
|
-
.option("--review", "
|
|
108
|
+
.option("--review [mode]", "Quality review: enhanced (~10-14% tokens) or ultimate (~20-30%)")
|
|
109
|
+
.option("--no-review", "Disable code review")
|
|
109
110
|
.action(async (opts) => runCommand({ ...opts, projectDir: await resolveAndValidateProjectDir() }));
|
|
110
111
|
void program.parseAsync();
|
|
111
112
|
//# sourceMappingURL=cli.js.map
|
package/dist/commands/run.js
CHANGED
|
@@ -24,10 +24,13 @@ async function executeRun(options) {
|
|
|
24
24
|
if (platform.experimental) {
|
|
25
25
|
console.log(chalk.yellow(`Warning: ${platform.displayName} support is experimental`));
|
|
26
26
|
}
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
27
|
+
const reviewMode = await resolveReviewMode(options.review, platform);
|
|
28
|
+
if (reviewMode === "enhanced") {
|
|
29
29
|
console.log(chalk.cyan("Enhanced mode: code review every 5 implementation loops"));
|
|
30
30
|
}
|
|
31
|
+
else if (reviewMode === "ultimate") {
|
|
32
|
+
console.log(chalk.cyan("Ultimate mode: code review after every completed story"));
|
|
33
|
+
}
|
|
31
34
|
const interval = parseInterval(options.interval);
|
|
32
35
|
let useDashboard = dashboard;
|
|
33
36
|
if (useDashboard) {
|
|
@@ -43,10 +46,10 @@ async function executeRun(options) {
|
|
|
43
46
|
}
|
|
44
47
|
const ralph = spawnRalphLoop(projectDir, platform.id, {
|
|
45
48
|
inheritStdio: !useDashboard,
|
|
46
|
-
|
|
49
|
+
reviewMode,
|
|
47
50
|
});
|
|
48
51
|
if (useDashboard) {
|
|
49
|
-
await startRunDashboard({ projectDir, interval, ralph,
|
|
52
|
+
await startRunDashboard({ projectDir, interval, ralph, reviewMode });
|
|
50
53
|
if (ralph.state === "stopped") {
|
|
51
54
|
applyRalphExitCode(ralph.exitCode);
|
|
52
55
|
}
|
|
@@ -70,34 +73,44 @@ function resolvePlatform(driverOverride, configPlatform) {
|
|
|
70
73
|
}
|
|
71
74
|
return getPlatform(id);
|
|
72
75
|
}
|
|
76
|
+
const VALID_REVIEW_MODES = new Set(["enhanced", "ultimate"]);
|
|
73
77
|
async function resolveReviewMode(reviewFlag, platform) {
|
|
74
|
-
if (reviewFlag ===
|
|
78
|
+
if (reviewFlag === false) {
|
|
79
|
+
return "off";
|
|
80
|
+
}
|
|
81
|
+
if (reviewFlag === true || typeof reviewFlag === "string") {
|
|
75
82
|
if (platform.id !== "claude-code") {
|
|
76
83
|
throw new Error("--review requires Claude Code (other drivers lack read-only enforcement)");
|
|
77
84
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
if (reviewFlag === true) {
|
|
86
|
+
return "enhanced";
|
|
87
|
+
}
|
|
88
|
+
if (!VALID_REVIEW_MODES.has(reviewFlag)) {
|
|
89
|
+
throw new Error(`Unknown review mode: ${reviewFlag}. Valid modes: enhanced, ultimate`);
|
|
90
|
+
}
|
|
91
|
+
return reviewFlag;
|
|
82
92
|
}
|
|
83
93
|
if (platform.id !== "claude-code") {
|
|
84
|
-
return
|
|
94
|
+
return "off";
|
|
85
95
|
}
|
|
86
96
|
if (!process.stdin.isTTY) {
|
|
87
|
-
return
|
|
97
|
+
return "off";
|
|
88
98
|
}
|
|
89
99
|
const { default: select } = await import("@inquirer/select");
|
|
90
|
-
|
|
100
|
+
return select({
|
|
91
101
|
message: "Quality mode:",
|
|
92
102
|
choices: [
|
|
93
|
-
{ name: "Standard —
|
|
103
|
+
{ name: "Standard — no code review (no extra cost)", value: "off" },
|
|
94
104
|
{
|
|
95
105
|
name: "Enhanced — periodic code review every 5 loops (~10-14% more tokens)",
|
|
96
106
|
value: "enhanced",
|
|
97
107
|
},
|
|
108
|
+
{
|
|
109
|
+
name: "Ultimate — review after every completed story (~20-30% more tokens)",
|
|
110
|
+
value: "ultimate",
|
|
111
|
+
},
|
|
98
112
|
],
|
|
99
|
-
default: "
|
|
113
|
+
default: "off",
|
|
100
114
|
});
|
|
101
|
-
return mode === "enhanced";
|
|
102
115
|
}
|
|
103
116
|
//# sourceMappingURL=run.js.map
|
|
@@ -55,6 +55,25 @@ const REVIEW_TEMPLATE_BLOCK = `# ===============================================
|
|
|
55
55
|
# PERIODIC CODE REVIEW
|
|
56
56
|
# =============================================================================
|
|
57
57
|
|
|
58
|
+
# Review mode: off, enhanced, or ultimate (set via 'bmalph run --review [mode]')
|
|
59
|
+
# - off: no code review (default)
|
|
60
|
+
# - enhanced: periodic review every REVIEW_INTERVAL loops (~10-14% more tokens)
|
|
61
|
+
# - ultimate: review after every completed story (~20-30% more tokens)
|
|
62
|
+
# The review agent analyzes git diffs and outputs findings for the next implementation loop.
|
|
63
|
+
# Currently supported on Claude Code only.
|
|
64
|
+
REVIEW_MODE="\${REVIEW_MODE:-off}"
|
|
65
|
+
|
|
66
|
+
# (Legacy) Enables review — prefer REVIEW_MODE instead
|
|
67
|
+
REVIEW_ENABLED="\${REVIEW_ENABLED:-false}"
|
|
68
|
+
|
|
69
|
+
# Number of implementation loops between review sessions (enhanced mode only)
|
|
70
|
+
REVIEW_INTERVAL="\${REVIEW_INTERVAL:-5}"
|
|
71
|
+
|
|
72
|
+
`;
|
|
73
|
+
const PREVIOUS_REVIEW_TEMPLATE_BLOCK = `# =============================================================================
|
|
74
|
+
# PERIODIC CODE REVIEW
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
58
77
|
# Enable periodic code review loops (set via 'bmalph run --review' or manually)
|
|
59
78
|
# When enabled, Ralph runs a read-only review session every REVIEW_INTERVAL loops.
|
|
60
79
|
# The review agent analyzes git diffs and outputs findings for the next implementation loop.
|
|
@@ -229,11 +248,21 @@ async function isRalphrcCustomized(filePath, platformId) {
|
|
|
229
248
|
if (matchesManagedPermissionVariants(content, templateWithoutReview)) {
|
|
230
249
|
return false;
|
|
231
250
|
}
|
|
251
|
+
// Check variants with previous review block (pre-ultimate installs)
|
|
252
|
+
const templateWithPreviousReview = currentTemplate.replace(REVIEW_TEMPLATE_BLOCK, PREVIOUS_REVIEW_TEMPLATE_BLOCK);
|
|
253
|
+
if (matchesManagedPermissionVariants(content, templateWithPreviousReview)) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
232
256
|
// Check variants without both quality gates and review blocks
|
|
233
257
|
const templateWithoutQGAndReview = templateWithoutQG.replace(REVIEW_TEMPLATE_BLOCK, "");
|
|
234
258
|
if (matchesManagedPermissionVariants(content, templateWithoutQGAndReview)) {
|
|
235
259
|
return false;
|
|
236
260
|
}
|
|
261
|
+
// Check variants without quality gates but with previous review block
|
|
262
|
+
const templateWithoutQGButPreviousReview = templateWithoutQG.replace(REVIEW_TEMPLATE_BLOCK, PREVIOUS_REVIEW_TEMPLATE_BLOCK);
|
|
263
|
+
if (matchesManagedPermissionVariants(content, templateWithoutQGButPreviousReview)) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
237
266
|
const legacyTemplate = normalizeManagedRalphrcContent(renderLegacyRalphrcTemplate(platformId));
|
|
238
267
|
return content !== legacyTemplate;
|
|
239
268
|
}
|
|
@@ -125,9 +125,14 @@ export async function validateRalphLoop(projectDir) {
|
|
|
125
125
|
}
|
|
126
126
|
export function spawnRalphLoop(projectDir, platformId, options) {
|
|
127
127
|
const env = { ...process.env, PLATFORM_DRIVER: platformId };
|
|
128
|
-
if (options.
|
|
129
|
-
env.
|
|
130
|
-
|
|
128
|
+
if (options.reviewMode) {
|
|
129
|
+
env.REVIEW_MODE = options.reviewMode;
|
|
130
|
+
if (options.reviewMode !== "off") {
|
|
131
|
+
env.REVIEW_ENABLED = "true";
|
|
132
|
+
if (options.reviewMode === "enhanced") {
|
|
133
|
+
env.REVIEW_INTERVAL = "5";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
131
136
|
}
|
|
132
137
|
const child = spawn(cachedBashCommand ?? "bash", [BASH_RALPH_LOOP_PATH], {
|
|
133
138
|
cwd: projectDir,
|
|
@@ -2,9 +2,9 @@ import { createRefreshCallback } from "../watch/dashboard.js";
|
|
|
2
2
|
import { createTerminalFrameWriter } from "../watch/frame-writer.js";
|
|
3
3
|
import { FileWatcher } from "../watch/file-watcher.js";
|
|
4
4
|
import { renderFooterLine } from "../watch/renderer.js";
|
|
5
|
-
export function renderStatusBar(ralph,
|
|
5
|
+
export function renderStatusBar(ralph, reviewMode) {
|
|
6
6
|
const pid = ralph.child.pid ?? "?";
|
|
7
|
-
const badge =
|
|
7
|
+
const badge = reviewMode === "ultimate" ? " [ultimate]" : reviewMode === "enhanced" ? " [review]" : "";
|
|
8
8
|
switch (ralph.state) {
|
|
9
9
|
case "running":
|
|
10
10
|
return `Ralph: running (PID ${pid})${badge} | q: stop/detach`;
|
|
@@ -18,12 +18,12 @@ export function renderQuitPrompt() {
|
|
|
18
18
|
return "Stop (s) | Detach (d) | Cancel (c)";
|
|
19
19
|
}
|
|
20
20
|
export async function startRunDashboard(options) {
|
|
21
|
-
const { projectDir, interval, ralph,
|
|
21
|
+
const { projectDir, interval, ralph, reviewMode } = options;
|
|
22
22
|
const frameWriter = createTerminalFrameWriter();
|
|
23
23
|
let showingPrompt = false;
|
|
24
24
|
let stopped = false;
|
|
25
25
|
const footerRenderer = (lastUpdated, cols) => {
|
|
26
|
-
const leftText = showingPrompt ? renderQuitPrompt() : renderStatusBar(ralph,
|
|
26
|
+
const leftText = showingPrompt ? renderQuitPrompt() : renderStatusBar(ralph, reviewMode);
|
|
27
27
|
return renderFooterLine(leftText, `Updated: ${lastUpdated.toISOString().slice(11, 19)}`, cols);
|
|
28
28
|
};
|
|
29
29
|
const refresh = createRefreshCallback(projectDir, (frame) => {
|
package/package.json
CHANGED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# Ralph Driver Interface Contract
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Ralph loop loads a platform driver by sourcing `ralph/drivers/${PLATFORM_DRIVER}.sh`
|
|
6
|
+
inside `load_platform_driver()` (`ralph_loop.sh` line 296). The `PLATFORM_DRIVER` variable
|
|
7
|
+
defaults to `"claude-code"` and can be overridden via `.ralphrc`.
|
|
8
|
+
|
|
9
|
+
After sourcing, `ralph_loop.sh` immediately calls three functions to populate core globals:
|
|
10
|
+
|
|
11
|
+
1. `driver_valid_tools` -- populates `VALID_TOOL_PATTERNS`
|
|
12
|
+
2. `driver_cli_binary` -- stored in `CLAUDE_CODE_CMD`
|
|
13
|
+
3. `driver_display_name` -- stored in `DRIVER_DISPLAY_NAME`
|
|
14
|
+
|
|
15
|
+
**File naming convention:** `${PLATFORM_DRIVER}.sh` (e.g., `claude-code.sh`, `codex.sh`).
|
|
16
|
+
|
|
17
|
+
**Scope:** This documents the sourceable driver contract used by `ralph_loop.sh`. Helper
|
|
18
|
+
scripts like `cursor-agent-wrapper.sh` are out of scope.
|
|
19
|
+
|
|
20
|
+
**Calling conventions:**
|
|
21
|
+
|
|
22
|
+
- Data is returned via stdout (`echo`).
|
|
23
|
+
- Booleans are returned via exit status (`0` = true, `1` = false).
|
|
24
|
+
- Some functions mutate global arrays as side effects.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Required Hooks
|
|
29
|
+
|
|
30
|
+
Called unconditionally by `ralph_loop.sh` with no `declare -F` guard or default stub.
|
|
31
|
+
Omitting any of these will break the loop at runtime.
|
|
32
|
+
|
|
33
|
+
### `driver_name()`
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
driver_name()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
No arguments. Echo a short lowercase identifier (e.g., `"claude-code"`, `"codex"`).
|
|
40
|
+
Used at line 2382 to gate platform-specific logic.
|
|
41
|
+
|
|
42
|
+
### `driver_display_name()`
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
driver_display_name()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
No arguments. Echo a human-readable name (e.g., `"Claude Code"`, `"OpenAI Codex"`).
|
|
49
|
+
Stored in `DRIVER_DISPLAY_NAME`, used in log messages and tmux pane titles.
|
|
50
|
+
|
|
51
|
+
### `driver_cli_binary()`
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
driver_cli_binary()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
No arguments. Echo the CLI executable name or resolved path (e.g., `"claude"`, `"codex"`).
|
|
58
|
+
Stored in `CLAUDE_CODE_CMD`. Most drivers return a static string; cursor resolves
|
|
59
|
+
dynamically.
|
|
60
|
+
|
|
61
|
+
### `driver_valid_tools()`
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
driver_valid_tools()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
No arguments. Must populate the global `VALID_TOOL_PATTERNS` array with the platform's
|
|
68
|
+
recognized tool name patterns. Used by `validate_allowed_tools()`.
|
|
69
|
+
|
|
70
|
+
### `driver_build_command(prompt_file, loop_context, session_id)`
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
driver_build_command "$prompt_file" "$loop_context" "$session_id"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Three string arguments:
|
|
77
|
+
|
|
78
|
+
| Argument | Description |
|
|
79
|
+
|----------|-------------|
|
|
80
|
+
| `$1` prompt_file | Path to the prompt file (e.g., `.ralph/PROMPT.md`) |
|
|
81
|
+
| `$2` loop_context | Context string for session continuity (may be empty) |
|
|
82
|
+
| `$3` session_id | Session ID for resume (empty string = new session) |
|
|
83
|
+
|
|
84
|
+
Must populate the global `CLAUDE_CMD_ARGS` array with the complete CLI command and
|
|
85
|
+
arguments. Return `0` on success, `1` on failure (e.g., prompt file not found).
|
|
86
|
+
|
|
87
|
+
**Reads globals:** `CLAUDE_OUTPUT_FORMAT`, `CLAUDE_PERMISSION_MODE` (claude-code only),
|
|
88
|
+
`CLAUDE_ALLOWED_TOOLS` (claude-code only), `CLAUDE_USE_CONTINUE`.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Optional Overrides with Loop Defaults
|
|
93
|
+
|
|
94
|
+
`ralph_loop.sh` defines default stubs at lines 284 and 288. All existing drivers override
|
|
95
|
+
them, but a minimal driver can rely on the defaults.
|
|
96
|
+
|
|
97
|
+
### `driver_supports_tool_allowlist()`
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
driver_supports_tool_allowlist()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
No arguments. Return `0` if the driver supports `--allowedTools` filtering, `1` otherwise.
|
|
104
|
+
|
|
105
|
+
**Default:** returns `1` (false). Currently only `claude-code` returns `0`.
|
|
106
|
+
|
|
107
|
+
### `driver_permission_denial_help()`
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
driver_permission_denial_help()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
No arguments. Print platform-specific troubleshooting guidance when the loop detects a
|
|
114
|
+
permission denial.
|
|
115
|
+
|
|
116
|
+
**Reads:** `RALPHRC_FILE`, `DRIVER_DISPLAY_NAME`.
|
|
117
|
+
|
|
118
|
+
**Default:** generic guidance text.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Optional Capability Hooks
|
|
123
|
+
|
|
124
|
+
Guarded by `declare -F` checks or wrapper functions in `ralph_loop.sh` (lines 1917-1954,
|
|
125
|
+
1576-1583). Safe to omit -- documented fallback behavior applies.
|
|
126
|
+
|
|
127
|
+
### `driver_supports_sessions()`
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
driver_supports_sessions()
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
No arguments. Return `0` if the driver supports session resume, `1` otherwise.
|
|
134
|
+
|
|
135
|
+
**If not defined:** assumed true (`0`).
|
|
136
|
+
|
|
137
|
+
Implemented by all 5 drivers; `copilot` returns `1`.
|
|
138
|
+
|
|
139
|
+
### `driver_supports_live_output()`
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
driver_supports_live_output()
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
No arguments. Return `0` if the driver supports structured streaming output (stream-json
|
|
146
|
+
or JSONL), `1` otherwise.
|
|
147
|
+
|
|
148
|
+
**If not defined:** assumed true (`0`).
|
|
149
|
+
|
|
150
|
+
`copilot` returns `1`; all others return `0`.
|
|
151
|
+
|
|
152
|
+
### `driver_prepare_live_command()`
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
driver_prepare_live_command()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
No arguments. Transform `CLAUDE_CMD_ARGS` into `LIVE_CMD_ARGS` for streaming mode.
|
|
159
|
+
|
|
160
|
+
**If not defined:** `LIVE_CMD_ARGS` is copied from `CLAUDE_CMD_ARGS` unchanged.
|
|
161
|
+
|
|
162
|
+
| Driver | Behavior |
|
|
163
|
+
|--------|----------|
|
|
164
|
+
| claude-code | Replaces `json` with `stream-json` and adds `--verbose --include-partial-messages` |
|
|
165
|
+
| codex | Copies as-is (output is already suitable) |
|
|
166
|
+
| opencode | Copies as-is (output is already suitable) |
|
|
167
|
+
| cursor | Replaces `json` with `stream-json` |
|
|
168
|
+
|
|
169
|
+
### `driver_stream_filter()`
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
driver_stream_filter()
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
No arguments. Echo a `jq` filter expression that transforms raw streaming events into
|
|
176
|
+
displayable text.
|
|
177
|
+
|
|
178
|
+
**If not defined:** returns `"empty"` (no output).
|
|
179
|
+
|
|
180
|
+
Each driver has a platform-specific filter; `copilot` returns `'.'` (passthrough).
|
|
181
|
+
|
|
182
|
+
### `driver_extract_session_id_from_output(output_file)`
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
driver_extract_session_id_from_output "$output_file"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
One argument: path to the CLI output log file. Echo the extracted session ID.
|
|
189
|
+
|
|
190
|
+
Tried first in the session save chain before the generic `jq` extractor. Only `opencode`
|
|
191
|
+
implements this (uses `sed` to extract from a `"session"` JSON object).
|
|
192
|
+
|
|
193
|
+
### `driver_fallback_session_id(output_file)`
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
driver_fallback_session_id "$output_file"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
One argument: path to the output file (caller passes it at line 1583; the only
|
|
200
|
+
implementation in `opencode` ignores it).
|
|
201
|
+
|
|
202
|
+
Last-resort session ID recovery when both driver-specific and generic extractors fail.
|
|
203
|
+
Only `opencode` implements this (queries `opencode session list --format json`).
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Conventional Metadata Hooks
|
|
208
|
+
|
|
209
|
+
Present in every driver but NOT called by `ralph_loop.sh`. Consumed by bmalph's TypeScript
|
|
210
|
+
doctor/preflight checks in `src/platform/`. A new driver should implement these for
|
|
211
|
+
`bmalph doctor` compatibility.
|
|
212
|
+
|
|
213
|
+
### `driver_min_version()`
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
driver_min_version()
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
No arguments. Echo the minimum required CLI version as a semver string.
|
|
220
|
+
|
|
221
|
+
### `driver_check_available()`
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
driver_check_available()
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
No arguments. Return `0` if the CLI binary is installed and reachable, `1` otherwise.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Global Variables
|
|
232
|
+
|
|
233
|
+
### Written by drivers
|
|
234
|
+
|
|
235
|
+
| Variable | Written by | Type | Description |
|
|
236
|
+
|----------------------|----------------------------------|-------|------------------------------------------------------|
|
|
237
|
+
| `VALID_TOOL_PATTERNS`| `driver_valid_tools()` | array | Valid tool name patterns for allowlist validation |
|
|
238
|
+
| `CLAUDE_CMD_ARGS` | `driver_build_command()` | array | Complete CLI command with all arguments |
|
|
239
|
+
| `LIVE_CMD_ARGS` | `driver_prepare_live_command()` | array | Modified command for live streaming |
|
|
240
|
+
|
|
241
|
+
### Read by drivers (set by ralph_loop.sh or .ralphrc)
|
|
242
|
+
|
|
243
|
+
| Variable | Used in | Description |
|
|
244
|
+
|-------------------------|--------------------------------------------|------------------------------------------------|
|
|
245
|
+
| `CLAUDE_OUTPUT_FORMAT` | `driver_build_command()` | `"json"` or `"text"` |
|
|
246
|
+
| `CLAUDE_PERMISSION_MODE`| `driver_build_command()` (claude-code) | Permission mode flag, default `"bypassPermissions"` |
|
|
247
|
+
| `CLAUDE_ALLOWED_TOOLS` | `driver_build_command()` (claude-code) | Comma-separated tool allowlist |
|
|
248
|
+
| `CLAUDE_USE_CONTINUE` | `driver_build_command()` | `"true"` or `"false"`, gates session resume |
|
|
249
|
+
| `RALPHRC_FILE` | `driver_permission_denial_help()` | Path to `.ralphrc` config file |
|
|
250
|
+
| `DRIVER_DISPLAY_NAME` | `driver_permission_denial_help()` | Human-readable driver name |
|
|
251
|
+
|
|
252
|
+
### Environment globals (cursor-specific)
|
|
253
|
+
|
|
254
|
+
| Variable | Used in | Description |
|
|
255
|
+
|---------------|--------------------------------------|-------------------------------------|
|
|
256
|
+
| `OS`, `OSTYPE`| `driver_running_on_windows()` | OS detection |
|
|
257
|
+
| `LOCALAPPDATA`| `driver_localappdata_cli_binary()` | Windows local app data path |
|
|
258
|
+
| `PATH` | `driver_find_windows_path_candidate()`| Manual PATH scanning on Windows |
|
|
259
|
+
|
|
260
|
+
### Set by ralph_loop.sh from driver output
|
|
261
|
+
|
|
262
|
+
| Variable | Source | Description |
|
|
263
|
+
|----------------------|-------------------------|-------------------------------|
|
|
264
|
+
| `CLAUDE_CODE_CMD` | `driver_cli_binary()` | CLI binary name/path |
|
|
265
|
+
| `DRIVER_DISPLAY_NAME`| `driver_display_name()` | Human-readable display name |
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Capability Matrix
|
|
270
|
+
|
|
271
|
+
| Capability | claude-code | codex | opencode | copilot | cursor |
|
|
272
|
+
|---------------------------------------------------------|:-----------:|:-----------:|:-----------:|:-----------:|:-----------:|
|
|
273
|
+
| Tool allowlist (`driver_supports_tool_allowlist`) | yes | no | no | no | no |
|
|
274
|
+
| Session continuity (`driver_supports_sessions`) | yes | yes | yes | no | yes |
|
|
275
|
+
| Structured live output (`driver_supports_live_output`) | yes | yes | yes | no | yes |
|
|
276
|
+
| Live command transform (`driver_prepare_live_command`) | transform | passthrough | passthrough | -- | transform |
|
|
277
|
+
| Stream filter (`driver_stream_filter`) | complex jq | JSONL select| JSONL select| passthrough | complex jq |
|
|
278
|
+
| Custom session extraction (`driver_extract_session_id_from_output`) | -- | -- | yes | -- | -- |
|
|
279
|
+
| Fallback session lookup (`driver_fallback_session_id`) | -- | -- | yes | -- | -- |
|
|
280
|
+
| Dynamic binary resolution (`driver_cli_binary`) | static | static | static | static | dynamic |
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Creating a New Driver
|
|
285
|
+
|
|
286
|
+
### Minimal driver skeleton
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
#!/usr/bin/env bash
|
|
290
|
+
# ralph/drivers/my-platform.sh
|
|
291
|
+
# Driver for My Platform CLI
|
|
292
|
+
#
|
|
293
|
+
# Sourced by ralph_loop.sh via load_platform_driver().
|
|
294
|
+
# PLATFORM_DRIVER must be set to "my-platform" in .ralphrc.
|
|
295
|
+
|
|
296
|
+
# ---------------------------------------------------------------------------
|
|
297
|
+
# Required hooks (5) -- omitting any of these breaks the loop
|
|
298
|
+
# ---------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
# Short lowercase identifier used to gate platform-specific logic.
|
|
301
|
+
driver_name() {
|
|
302
|
+
echo "my-platform"
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Human-readable name for log messages and tmux pane titles.
|
|
306
|
+
driver_display_name() {
|
|
307
|
+
echo "My Platform"
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# CLI executable name or resolved path.
|
|
311
|
+
driver_cli_binary() {
|
|
312
|
+
echo "my-platform"
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# Populate VALID_TOOL_PATTERNS with recognized tool name patterns.
|
|
316
|
+
# Used by validate_allowed_tools() to check allowlist entries.
|
|
317
|
+
driver_valid_tools() {
|
|
318
|
+
VALID_TOOL_PATTERNS=(
|
|
319
|
+
"Read"
|
|
320
|
+
"Write"
|
|
321
|
+
"Edit"
|
|
322
|
+
"Bash"
|
|
323
|
+
# Add your platform's tool patterns here
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
# Build the complete CLI command array.
|
|
328
|
+
# $1 = prompt_file Path to .ralph/PROMPT.md
|
|
329
|
+
# $2 = loop_context Context string for session continuity (may be empty)
|
|
330
|
+
# $3 = session_id Session ID for resume (empty = new session)
|
|
331
|
+
driver_build_command() {
|
|
332
|
+
local prompt_file="$1"
|
|
333
|
+
local loop_context="$2"
|
|
334
|
+
local session_id="$3"
|
|
335
|
+
|
|
336
|
+
if [[ ! -f "$prompt_file" ]]; then
|
|
337
|
+
return 1
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
CLAUDE_CMD_ARGS=(
|
|
341
|
+
"my-platform"
|
|
342
|
+
"--prompt" "$prompt_file"
|
|
343
|
+
"--output-format" "${CLAUDE_OUTPUT_FORMAT:-json}"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Append session resume flag if continuing a session
|
|
347
|
+
if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then
|
|
348
|
+
CLAUDE_CMD_ARGS+=("--session" "$session_id")
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
# Append context if provided
|
|
352
|
+
if [[ -n "$loop_context" ]]; then
|
|
353
|
+
CLAUDE_CMD_ARGS+=("--context" "$loop_context")
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
return 0
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
# Optional overrides (2) -- loop provides default stubs
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
# Return 0 if the platform supports --allowedTools filtering, 1 otherwise.
|
|
364
|
+
driver_supports_tool_allowlist() {
|
|
365
|
+
return 1
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
# Print troubleshooting guidance on permission denial.
|
|
369
|
+
driver_permission_denial_help() {
|
|
370
|
+
echo "Permission denied. Check that $DRIVER_DISPLAY_NAME has the required permissions."
|
|
371
|
+
echo "See $RALPHRC_FILE for configuration options."
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# ---------------------------------------------------------------------------
|
|
375
|
+
# Metadata hooks (2) -- used by bmalph doctor, not called by ralph_loop.sh
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
# Minimum required CLI version (semver).
|
|
379
|
+
driver_min_version() {
|
|
380
|
+
echo "1.0.0"
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
# Return 0 if the CLI binary is installed and reachable, 1 otherwise.
|
|
384
|
+
driver_check_available() {
|
|
385
|
+
command -v my-platform &>/dev/null
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Checklist
|
|
390
|
+
|
|
391
|
+
- [ ] All 5 required hooks implemented (`driver_name`, `driver_display_name`,
|
|
392
|
+
`driver_cli_binary`, `driver_valid_tools`, `driver_build_command`)
|
|
393
|
+
- [ ] `driver_valid_tools` populates `VALID_TOOL_PATTERNS` with your platform's tool names
|
|
394
|
+
- [ ] `driver_build_command` handles all three arguments correctly
|
|
395
|
+
(`prompt_file`, `loop_context`, `session_id`)
|
|
396
|
+
- [ ] `driver_check_available` returns `0` only when the CLI is installed
|
|
397
|
+
- [ ] File named `${platform_id}.sh` matching the `PLATFORM_DRIVER` value in `.ralphrc`
|
|
398
|
+
- [ ] Register corresponding platform definition in `src/platform/` for bmalph CLI integration
|
|
399
|
+
- [ ] Tested with `bmalph doctor`
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Session ID Recovery Chain
|
|
404
|
+
|
|
405
|
+
When the loop needs to persist a session ID for resume, it follows a three-step priority
|
|
406
|
+
chain (`ralph_loop.sh` lines 1574-1588):
|
|
407
|
+
|
|
408
|
+
1. **`driver_extract_session_id_from_output($output_file)`** -- Driver-specific extraction.
|
|
409
|
+
If the function exists (`declare -F` guard) and echoes a non-empty string, that value
|
|
410
|
+
is used. Only `opencode` implements this (uses `sed` to extract from a `"session"` JSON
|
|
411
|
+
object).
|
|
412
|
+
|
|
413
|
+
2. **`extract_session_id_from_output($output_file)`** -- Generic `jq` extractor from
|
|
414
|
+
`response_analyzer.sh`. Searches the output file for `.sessionId`,
|
|
415
|
+
`.metadata.session_id`, and `.session_id` in that order.
|
|
416
|
+
|
|
417
|
+
3. **`driver_fallback_session_id($output_file)`** -- CLI-based last-resort recovery. If the
|
|
418
|
+
function exists and the previous steps produced nothing, this is called. Only `opencode`
|
|
419
|
+
implements this (queries `opencode session list --format json`).
|
|
420
|
+
|
|
421
|
+
The first step that returns a non-empty string wins. If all three steps fail, no session ID
|
|
422
|
+
is saved and the next iteration starts a fresh session.
|
package/ralph/drivers/codex.sh
CHANGED
|
@@ -46,7 +46,7 @@ driver_permission_denial_help() {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
# Build Codex CLI command
|
|
49
|
-
# Codex uses: codex exec [
|
|
49
|
+
# Codex uses: codex exec [resume <id>] --json "prompt"
|
|
50
50
|
driver_build_command() {
|
|
51
51
|
local prompt_file=$1
|
|
52
52
|
local loop_context=$2
|
|
@@ -67,7 +67,7 @@ driver_build_command() {
|
|
|
67
67
|
|
|
68
68
|
# Session resume — gated on CLAUDE_USE_CONTINUE to respect --no-continue flag
|
|
69
69
|
if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then
|
|
70
|
-
CLAUDE_CMD_ARGS+=("
|
|
70
|
+
CLAUDE_CMD_ARGS+=("resume" "$session_id")
|
|
71
71
|
fi
|
|
72
72
|
|
|
73
73
|
# Build prompt with context
|
|
@@ -762,14 +762,14 @@ parse_json_response() {
|
|
|
762
762
|
local summary_has_no_work_pattern="false"
|
|
763
763
|
if [[ "$response_shape" == "codex_jsonl" || "$response_shape" == "opencode_jsonl" || "$response_shape" == "cursor_stream_jsonl" ]] && [[ "$explicit_exit_signal_found" != "true" && -n "$summary" ]]; then
|
|
764
764
|
for keyword in "${COMPLETION_KEYWORDS[@]}"; do
|
|
765
|
-
if echo "$summary" | grep -
|
|
765
|
+
if echo "$summary" | grep -qiw "$keyword"; then
|
|
766
766
|
summary_has_completion_keyword="true"
|
|
767
767
|
break
|
|
768
768
|
fi
|
|
769
769
|
done
|
|
770
770
|
|
|
771
771
|
for pattern in "${NO_WORK_PATTERNS[@]}"; do
|
|
772
|
-
if echo "$summary" | grep -
|
|
772
|
+
if echo "$summary" | grep -qiw "$pattern"; then
|
|
773
773
|
summary_has_no_work_pattern="true"
|
|
774
774
|
break
|
|
775
775
|
fi
|
|
@@ -1035,13 +1035,15 @@ analyze_response() {
|
|
|
1035
1035
|
|
|
1036
1036
|
# Text parsing fallback (original logic)
|
|
1037
1037
|
|
|
1038
|
-
#
|
|
1039
|
-
#
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1038
|
+
# 1. Check for explicit structured output (RALPH_STATUS block)
|
|
1039
|
+
# When a status block is present, it is authoritative — skip all heuristics.
|
|
1040
|
+
# A structurally valid but field-empty block results in exit_signal=false,
|
|
1041
|
+
# confidence=0 by design (AI produced a block but provided no signal).
|
|
1042
|
+
local ralph_status_block_found=false
|
|
1043
1043
|
local ralph_status_json=""
|
|
1044
1044
|
if ralph_status_json=$(extract_ralph_status_block_json "$output_content" 2>/dev/null); then
|
|
1045
|
+
ralph_status_block_found=true
|
|
1046
|
+
|
|
1045
1047
|
local status
|
|
1046
1048
|
status=$(printf '%s' "$ralph_status_json" | jq -r -j '.status' 2>/dev/null)
|
|
1047
1049
|
local exit_sig_found
|
|
@@ -1062,14 +1064,14 @@ analyze_response() {
|
|
|
1062
1064
|
|
|
1063
1065
|
# If EXIT_SIGNAL is explicitly provided, respect it
|
|
1064
1066
|
if [[ "$exit_sig_found" == "true" ]]; then
|
|
1065
|
-
explicit_exit_signal_found=true
|
|
1066
1067
|
if [[ "$exit_sig" == "true" ]]; then
|
|
1067
1068
|
has_completion_signal=true
|
|
1068
1069
|
exit_signal=true
|
|
1069
1070
|
confidence_score=100
|
|
1070
1071
|
else
|
|
1071
|
-
# Explicit EXIT_SIGNAL: false
|
|
1072
|
+
# Explicit EXIT_SIGNAL: false — Claude says to continue
|
|
1072
1073
|
exit_signal=false
|
|
1074
|
+
confidence_score=80
|
|
1073
1075
|
fi
|
|
1074
1076
|
elif [[ "$status" == "COMPLETE" ]]; then
|
|
1075
1077
|
# No explicit EXIT_SIGNAL but STATUS is COMPLETE
|
|
@@ -1077,68 +1079,93 @@ analyze_response() {
|
|
|
1077
1079
|
exit_signal=true
|
|
1078
1080
|
confidence_score=100
|
|
1079
1081
|
fi
|
|
1082
|
+
# is_test_only and is_stuck stay false (defaults) — status block is authoritative
|
|
1080
1083
|
fi
|
|
1081
1084
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
if grep -qi "$keyword" "$output_file"; then
|
|
1085
|
-
has_completion_signal=true
|
|
1086
|
-
((confidence_score+=10))
|
|
1087
|
-
break
|
|
1088
|
-
fi
|
|
1089
|
-
done
|
|
1085
|
+
if [[ "$ralph_status_block_found" != "true" ]]; then
|
|
1086
|
+
# No status block found — fall back to heuristic analysis
|
|
1090
1087
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1088
|
+
# 2. Detect completion keywords in natural language output
|
|
1089
|
+
for keyword in "${COMPLETION_KEYWORDS[@]}"; do
|
|
1090
|
+
if grep -qiw "$keyword" "$output_file"; then
|
|
1091
|
+
has_completion_signal=true
|
|
1092
|
+
((confidence_score+=10))
|
|
1093
|
+
break
|
|
1094
|
+
fi
|
|
1095
|
+
done
|
|
1095
1096
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1097
|
+
# 3. Detect test-only loops
|
|
1098
|
+
local test_command_count=0
|
|
1099
|
+
local implementation_count=0
|
|
1100
|
+
local error_count=0
|
|
1098
1101
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
implementation_count=$(echo "$implementation_count" | tr -d '[:space:]')
|
|
1102
|
+
test_command_count=$(grep -c -i "running tests\|npm test\|bats\|pytest\|jest" "$output_file" 2>/dev/null | head -1 || echo "0")
|
|
1103
|
+
implementation_count=$(grep -c -i "implementing\|creating\|writing\|adding\|function\|class" "$output_file" 2>/dev/null | head -1 || echo "0")
|
|
1102
1104
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
test_command_count=$((test_command_count + 0))
|
|
1107
|
-
implementation_count=$((implementation_count + 0))
|
|
1105
|
+
# Strip whitespace and ensure it's a number
|
|
1106
|
+
test_command_count=$(echo "$test_command_count" | tr -d '[:space:]')
|
|
1107
|
+
implementation_count=$(echo "$implementation_count" | tr -d '[:space:]')
|
|
1108
1108
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1109
|
+
# Convert to integers with default fallback
|
|
1110
|
+
test_command_count=${test_command_count:-0}
|
|
1111
|
+
implementation_count=${implementation_count:-0}
|
|
1112
|
+
test_command_count=$((test_command_count + 0))
|
|
1113
|
+
implementation_count=$((implementation_count + 0))
|
|
1113
1114
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
# Pattern aligned with ralph_loop.sh to ensure consistent behavior
|
|
1119
|
-
error_count=$(grep -v '"[^"]*error[^"]*":' "$output_file" 2>/dev/null | \
|
|
1120
|
-
grep -cE '(^Error:|^ERROR:|^error:|\]: error|Link: error|Error occurred|failed with error|[Ee]xception|Fatal|FATAL)' \
|
|
1121
|
-
2>/dev/null || echo "0")
|
|
1122
|
-
error_count=$(echo "$error_count" | tr -d '[:space:]')
|
|
1123
|
-
error_count=${error_count:-0}
|
|
1124
|
-
error_count=$((error_count + 0))
|
|
1115
|
+
if [[ $test_command_count -gt 0 ]] && [[ $implementation_count -eq 0 ]]; then
|
|
1116
|
+
is_test_only=true
|
|
1117
|
+
work_summary="Test execution only, no implementation"
|
|
1118
|
+
fi
|
|
1125
1119
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1120
|
+
# 4. Detect stuck/error loops
|
|
1121
|
+
# Use two-stage filtering to avoid counting JSON field names as errors
|
|
1122
|
+
# Stage 1: Filter out JSON field patterns like "is_error": false
|
|
1123
|
+
# Stage 2: Count actual error messages in specific contexts
|
|
1124
|
+
# Pattern aligned with ralph_loop.sh to ensure consistent behavior
|
|
1125
|
+
error_count=$(grep -v '"[^"]*error[^"]*":' "$output_file" 2>/dev/null | \
|
|
1126
|
+
grep -cE '(^Error:|^ERROR:|^error:|\]: error|Link: error|Error occurred|failed with error|[Ee]xception|Fatal|FATAL)' \
|
|
1127
|
+
2>/dev/null || echo "0")
|
|
1128
|
+
error_count=$(echo "$error_count" | tr -d '[:space:]')
|
|
1129
|
+
error_count=${error_count:-0}
|
|
1130
|
+
error_count=$((error_count + 0))
|
|
1131
|
+
|
|
1132
|
+
if [[ $error_count -gt 5 ]]; then
|
|
1133
|
+
is_stuck=true
|
|
1134
|
+
fi
|
|
1129
1135
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1136
|
+
# 5. Detect "nothing to do" patterns
|
|
1137
|
+
for pattern in "${NO_WORK_PATTERNS[@]}"; do
|
|
1138
|
+
if grep -qiw "$pattern" "$output_file"; then
|
|
1139
|
+
has_completion_signal=true
|
|
1140
|
+
((confidence_score+=15))
|
|
1141
|
+
work_summary="No work remaining"
|
|
1142
|
+
break
|
|
1143
|
+
fi
|
|
1144
|
+
done
|
|
1145
|
+
|
|
1146
|
+
# 7. Analyze output length trends (detect declining engagement)
|
|
1147
|
+
if [[ -f "$RALPH_DIR/.last_output_length" ]]; then
|
|
1148
|
+
local last_length
|
|
1149
|
+
last_length=$(cat "$RALPH_DIR/.last_output_length")
|
|
1150
|
+
if [[ "$last_length" -gt 0 ]]; then
|
|
1151
|
+
local length_ratio=$((output_length * 100 / last_length))
|
|
1152
|
+
if [[ $length_ratio -lt 50 ]]; then
|
|
1153
|
+
# Output is less than 50% of previous - possible completion
|
|
1154
|
+
((confidence_score+=10))
|
|
1155
|
+
fi
|
|
1156
|
+
fi
|
|
1137
1157
|
fi
|
|
1138
|
-
done
|
|
1139
1158
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1159
|
+
# 9. Determine exit signal based on confidence (heuristic)
|
|
1160
|
+
if [[ $confidence_score -ge 40 || "$has_completion_signal" == "true" ]]; then
|
|
1161
|
+
exit_signal=true
|
|
1162
|
+
fi
|
|
1163
|
+
fi
|
|
1164
|
+
|
|
1165
|
+
# Always persist output length for next iteration (both paths)
|
|
1166
|
+
echo "$output_length" > "$RALPH_DIR/.last_output_length"
|
|
1167
|
+
|
|
1168
|
+
# 6. Check for file changes (git integration) — always runs
|
|
1142
1169
|
if command -v git &>/dev/null && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
1143
1170
|
local loop_start_sha=""
|
|
1144
1171
|
local current_sha=""
|
|
@@ -1174,19 +1201,7 @@ analyze_response() {
|
|
|
1174
1201
|
fi
|
|
1175
1202
|
fi
|
|
1176
1203
|
|
|
1177
|
-
#
|
|
1178
|
-
if [[ -f "$RALPH_DIR/.last_output_length" ]]; then
|
|
1179
|
-
local last_length=$(cat "$RALPH_DIR/.last_output_length")
|
|
1180
|
-
local length_ratio=$((output_length * 100 / last_length))
|
|
1181
|
-
|
|
1182
|
-
if [[ $length_ratio -lt 50 ]]; then
|
|
1183
|
-
# Output is less than 50% of previous - possible completion
|
|
1184
|
-
((confidence_score+=10))
|
|
1185
|
-
fi
|
|
1186
|
-
fi
|
|
1187
|
-
echo "$output_length" > "$RALPH_DIR/.last_output_length"
|
|
1188
|
-
|
|
1189
|
-
# 8. Extract work summary from output
|
|
1204
|
+
# 8. Extract work summary from output — always runs
|
|
1190
1205
|
if [[ -z "$work_summary" ]]; then
|
|
1191
1206
|
# Try to find summary in output
|
|
1192
1207
|
work_summary=$(grep -i "summary\|completed\|implemented" "$output_file" | head -1 | cut -c 1-100)
|
|
@@ -1195,21 +1210,6 @@ analyze_response() {
|
|
|
1195
1210
|
fi
|
|
1196
1211
|
fi
|
|
1197
1212
|
|
|
1198
|
-
# Explicit EXIT_SIGNAL=false means "continue working", so completion
|
|
1199
|
-
# heuristics must not register a done signal.
|
|
1200
|
-
if [[ "$explicit_exit_signal_found" == "true" && "$exit_signal" == "false" ]]; then
|
|
1201
|
-
has_completion_signal=false
|
|
1202
|
-
fi
|
|
1203
|
-
|
|
1204
|
-
# 9. Determine exit signal based on confidence (heuristic)
|
|
1205
|
-
# IMPORTANT: Only apply heuristics if no explicit EXIT_SIGNAL was found in RALPH_STATUS
|
|
1206
|
-
# Claude's explicit intent takes precedence over natural language pattern matching
|
|
1207
|
-
if [[ "$explicit_exit_signal_found" != "true" ]]; then
|
|
1208
|
-
if [[ $confidence_score -ge 40 || "$has_completion_signal" == "true" ]]; then
|
|
1209
|
-
exit_signal=true
|
|
1210
|
-
fi
|
|
1211
|
-
fi
|
|
1212
|
-
|
|
1213
1213
|
local has_permission_denials=false
|
|
1214
1214
|
local permission_denial_count=0
|
|
1215
1215
|
local denied_commands_json='[]'
|
package/ralph/ralph_loop.sh
CHANGED
|
@@ -76,6 +76,7 @@ _env_QUALITY_GATE_TIMEOUT="${QUALITY_GATE_TIMEOUT:-}"
|
|
|
76
76
|
_env_QUALITY_GATE_ON_COMPLETION_ONLY="${QUALITY_GATE_ON_COMPLETION_ONLY:-}"
|
|
77
77
|
_env_REVIEW_ENABLED="${REVIEW_ENABLED:-}"
|
|
78
78
|
_env_REVIEW_INTERVAL="${REVIEW_INTERVAL:-}"
|
|
79
|
+
_env_REVIEW_MODE="${REVIEW_MODE:-}"
|
|
79
80
|
|
|
80
81
|
# Now set defaults (only if not already set by environment)
|
|
81
82
|
MAX_CALLS_PER_HOUR="${MAX_CALLS_PER_HOUR:-100}"
|
|
@@ -116,6 +117,11 @@ REVIEW_FINDINGS_FILE="$RALPH_DIR/.review_findings.json"
|
|
|
116
117
|
REVIEW_PROMPT_FILE="$RALPH_DIR/REVIEW_PROMPT.md"
|
|
117
118
|
REVIEW_LAST_SHA_FILE="$RALPH_DIR/.review_last_sha"
|
|
118
119
|
|
|
120
|
+
# REVIEW_MODE is derived in initialize_runtime_context() after .ralphrc is loaded.
|
|
121
|
+
# This ensures backwards compat: old .ralphrc files with only REVIEW_ENABLED=true
|
|
122
|
+
# still map to enhanced mode. Env vars always win via the snapshot/restore mechanism.
|
|
123
|
+
REVIEW_MODE="${REVIEW_MODE:-off}"
|
|
124
|
+
|
|
119
125
|
# Valid tool patterns for --allowed-tools validation
|
|
120
126
|
# Default: Claude Code tools. Platform driver overwrites via driver_valid_tools() in main().
|
|
121
127
|
# Validation runs in main() after load_platform_driver so the correct patterns are in effect.
|
|
@@ -267,6 +273,7 @@ load_ralphrc() {
|
|
|
267
273
|
[[ -n "$_env_QUALITY_GATE_ON_COMPLETION_ONLY" ]] && QUALITY_GATE_ON_COMPLETION_ONLY="$_env_QUALITY_GATE_ON_COMPLETION_ONLY"
|
|
268
274
|
[[ -n "$_env_REVIEW_ENABLED" ]] && REVIEW_ENABLED="$_env_REVIEW_ENABLED"
|
|
269
275
|
[[ -n "$_env_REVIEW_INTERVAL" ]] && REVIEW_INTERVAL="$_env_REVIEW_INTERVAL"
|
|
276
|
+
[[ -n "$_env_REVIEW_MODE" ]] && REVIEW_MODE="$_env_REVIEW_MODE"
|
|
270
277
|
|
|
271
278
|
normalize_claude_permission_mode
|
|
272
279
|
RALPHRC_FILE="$config_file"
|
|
@@ -317,6 +324,14 @@ initialize_runtime_context() {
|
|
|
317
324
|
fi
|
|
318
325
|
fi
|
|
319
326
|
|
|
327
|
+
# Derive REVIEW_MODE after .ralphrc load so backwards-compat works:
|
|
328
|
+
# old .ralphrc files with only REVIEW_ENABLED=true map to enhanced mode.
|
|
329
|
+
if [[ "$REVIEW_MODE" == "off" && "$REVIEW_ENABLED" == "true" ]]; then
|
|
330
|
+
REVIEW_MODE="enhanced"
|
|
331
|
+
fi
|
|
332
|
+
# Keep REVIEW_ENABLED in sync for any code that checks it
|
|
333
|
+
[[ "$REVIEW_MODE" != "off" ]] && REVIEW_ENABLED="true" || REVIEW_ENABLED="false"
|
|
334
|
+
|
|
320
335
|
# Load platform driver after config so PLATFORM_DRIVER can be overridden.
|
|
321
336
|
load_platform_driver
|
|
322
337
|
RUNTIME_CONTEXT_LOADED=true
|
|
@@ -357,7 +372,7 @@ get_tmux_base_index() {
|
|
|
357
372
|
# Setup tmux session with monitor
|
|
358
373
|
setup_tmux_session() {
|
|
359
374
|
local session_name="ralph-$(date +%s)"
|
|
360
|
-
local ralph_home="${RALPH_HOME:-$
|
|
375
|
+
local ralph_home="${RALPH_HOME:-$SCRIPT_DIR}"
|
|
361
376
|
local project_dir="$(pwd)"
|
|
362
377
|
|
|
363
378
|
initialize_runtime_context
|
|
@@ -1282,27 +1297,44 @@ build_loop_context() {
|
|
|
1282
1297
|
echo "${context:0:500}"
|
|
1283
1298
|
}
|
|
1284
1299
|
|
|
1285
|
-
# Check if a
|
|
1300
|
+
# Check if a code review should run this iteration
|
|
1286
1301
|
# Returns 0 (true) when review is due, 1 (false) otherwise
|
|
1302
|
+
# Args: $1 = loop_count, $2 = fix_plan_completed_delta (optional, for ultimate mode)
|
|
1287
1303
|
should_run_review() {
|
|
1288
|
-
[[ "$
|
|
1304
|
+
[[ "$REVIEW_MODE" == "off" ]] && return 1
|
|
1289
1305
|
local loop_count=$1
|
|
1306
|
+
local fix_plan_delta=${2:-0}
|
|
1307
|
+
|
|
1290
1308
|
# Never review on first loop (no implementation yet)
|
|
1291
1309
|
(( loop_count < 1 )) && return 1
|
|
1292
|
-
|
|
1310
|
+
|
|
1293
1311
|
# Skip if circuit breaker is not CLOSED
|
|
1294
1312
|
if [[ -f "$RALPH_DIR/.circuit_breaker_state" ]]; then
|
|
1295
1313
|
local cb_state
|
|
1296
1314
|
cb_state=$(jq -r '.state // "CLOSED"' "$RALPH_DIR/.circuit_breaker_state" 2>/dev/null)
|
|
1297
1315
|
[[ "$cb_state" != "CLOSED" ]] && return 1
|
|
1298
1316
|
fi
|
|
1317
|
+
|
|
1318
|
+
# Mode-specific trigger
|
|
1319
|
+
case "$REVIEW_MODE" in
|
|
1320
|
+
enhanced)
|
|
1321
|
+
(( loop_count % REVIEW_INTERVAL != 0 )) && return 1
|
|
1322
|
+
;;
|
|
1323
|
+
ultimate)
|
|
1324
|
+
(( fix_plan_delta < 1 )) && return 1
|
|
1325
|
+
;;
|
|
1326
|
+
*)
|
|
1327
|
+
# Unknown mode — treat as off
|
|
1328
|
+
return 1
|
|
1329
|
+
;;
|
|
1330
|
+
esac
|
|
1331
|
+
|
|
1299
1332
|
# Skip if no changes since last review (committed or uncommitted)
|
|
1300
1333
|
if command -v git &>/dev/null && git rev-parse --git-dir &>/dev/null 2>&1; then
|
|
1301
1334
|
local current_sha last_sha
|
|
1302
1335
|
current_sha=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
|
1303
1336
|
last_sha=""
|
|
1304
1337
|
[[ -f "$REVIEW_LAST_SHA_FILE" ]] && last_sha=$(cat "$REVIEW_LAST_SHA_FILE" 2>/dev/null)
|
|
1305
|
-
# Check for new commits OR uncommitted workspace changes
|
|
1306
1338
|
local has_uncommitted
|
|
1307
1339
|
has_uncommitted=$(git status --porcelain 2>/dev/null | head -1)
|
|
1308
1340
|
if [[ "$current_sha" == "$last_sha" && -z "$has_uncommitted" ]]; then
|
|
@@ -1313,7 +1345,8 @@ should_run_review() {
|
|
|
1313
1345
|
}
|
|
1314
1346
|
|
|
1315
1347
|
# Build review findings context for injection into the next implementation loop
|
|
1316
|
-
# Returns a compact string (max 500 chars) with unresolved findings
|
|
1348
|
+
# Returns a compact string (max 500-700 chars) with unresolved findings
|
|
1349
|
+
# HIGH/CRITICAL findings get a PRIORITY prefix and a higher char cap (700)
|
|
1317
1350
|
build_review_context() {
|
|
1318
1351
|
if [[ ! -f "$REVIEW_FINDINGS_FILE" ]]; then
|
|
1319
1352
|
echo ""
|
|
@@ -1330,7 +1363,15 @@ build_review_context() {
|
|
|
1330
1363
|
return
|
|
1331
1364
|
fi
|
|
1332
1365
|
|
|
1333
|
-
|
|
1366
|
+
# HIGH/CRITICAL findings: instruct the AI to fix them before picking a new story
|
|
1367
|
+
local context=""
|
|
1368
|
+
local max_len=500
|
|
1369
|
+
if [[ "$severity" == "HIGH" || "$severity" == "CRITICAL" ]]; then
|
|
1370
|
+
context="PRIORITY: Fix these code review findings BEFORE picking a new story. "
|
|
1371
|
+
max_len=700
|
|
1372
|
+
fi
|
|
1373
|
+
context+="REVIEW FINDINGS ($severity, $issues_found issues): $summary"
|
|
1374
|
+
|
|
1334
1375
|
# Include top details if space allows
|
|
1335
1376
|
local top_details
|
|
1336
1377
|
top_details=$(jq -r '(.details[:2] // []) | map("- [\(.severity)] \(.file): \(.issue)") | join("; ")' "$REVIEW_FINDINGS_FILE" 2>/dev/null | head -c 150)
|
|
@@ -1338,7 +1379,7 @@ build_review_context() {
|
|
|
1338
1379
|
context+=" Details: $top_details"
|
|
1339
1380
|
fi
|
|
1340
1381
|
|
|
1341
|
-
echo "${context:0
|
|
1382
|
+
echo "${context:0:$max_len}"
|
|
1342
1383
|
}
|
|
1343
1384
|
|
|
1344
1385
|
# Execute a periodic code review loop (read-only, no file modifications)
|
|
@@ -2499,8 +2540,18 @@ main() {
|
|
|
2499
2540
|
|
|
2500
2541
|
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "completed" "success"
|
|
2501
2542
|
|
|
2502
|
-
#
|
|
2503
|
-
|
|
2543
|
+
# Consume review findings after successful execution — the AI has received
|
|
2544
|
+
# the context via --append-system-prompt. Deleting here (not in
|
|
2545
|
+
# build_review_context) ensures findings survive transient loop failures.
|
|
2546
|
+
rm -f "$REVIEW_FINDINGS_FILE"
|
|
2547
|
+
|
|
2548
|
+
# Code review check
|
|
2549
|
+
local fix_plan_delta=0
|
|
2550
|
+
if [[ -f "$RESPONSE_ANALYSIS_FILE" ]]; then
|
|
2551
|
+
fix_plan_delta=$(jq -r '.analysis.fix_plan_completed_delta // 0' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "0")
|
|
2552
|
+
[[ ! "$fix_plan_delta" =~ ^-?[0-9]+$ ]] && fix_plan_delta=0
|
|
2553
|
+
fi
|
|
2554
|
+
if should_run_review "$loop_count" "$fix_plan_delta"; then
|
|
2504
2555
|
run_review_loop "$loop_count"
|
|
2505
2556
|
fi
|
|
2506
2557
|
|
|
@@ -129,13 +129,18 @@ QUALITY_GATE_ON_COMPLETION_ONLY="${QUALITY_GATE_ON_COMPLETION_ONLY:-false}"
|
|
|
129
129
|
# PERIODIC CODE REVIEW
|
|
130
130
|
# =============================================================================
|
|
131
131
|
|
|
132
|
-
#
|
|
133
|
-
#
|
|
132
|
+
# Review mode: off, enhanced, or ultimate (set via 'bmalph run --review [mode]')
|
|
133
|
+
# - off: no code review (default)
|
|
134
|
+
# - enhanced: periodic review every REVIEW_INTERVAL loops (~10-14% more tokens)
|
|
135
|
+
# - ultimate: review after every completed story (~20-30% more tokens)
|
|
134
136
|
# The review agent analyzes git diffs and outputs findings for the next implementation loop.
|
|
135
137
|
# Currently supported on Claude Code only.
|
|
138
|
+
REVIEW_MODE="${REVIEW_MODE:-off}"
|
|
139
|
+
|
|
140
|
+
# (Legacy) Enables review — prefer REVIEW_MODE instead
|
|
136
141
|
REVIEW_ENABLED="${REVIEW_ENABLED:-false}"
|
|
137
142
|
|
|
138
|
-
# Number of implementation loops between review sessions (
|
|
143
|
+
# Number of implementation loops between review sessions (enhanced mode only)
|
|
139
144
|
REVIEW_INTERVAL="${REVIEW_INTERVAL:-5}"
|
|
140
145
|
|
|
141
146
|
# =============================================================================
|