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 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 | Description |
283
- | ---------------------- | --------------------------------------------------------------------------- |
284
- | `--driver <platform>` | Override platform driver (claude-code, codex, opencode, copilot, cursor) |
285
- | `--review/--no-review` | Enable/disable periodic code review (Claude Code only, prompted by default) |
286
- | `--interval <ms>` | Dashboard refresh interval in milliseconds (default: 2000) |
287
- | `--no-dashboard` | Run Ralph without the dashboard overlay |
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 periodic review every 5 loops (`--review`, Claude Code only). A read-only session analyzes git diffs and feeds structured findings into the next implementation loop
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", "Enable periodic code review loop (~10-14% more tokens)")
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
@@ -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 reviewEnabled = await resolveReviewMode(options.review, platform);
28
- if (reviewEnabled) {
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
- ...(reviewEnabled && { reviewEnabled }),
49
+ reviewMode,
47
50
  });
48
51
  if (useDashboard) {
49
- await startRunDashboard({ projectDir, interval, ralph, reviewEnabled });
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 === true) {
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
- return true;
79
- }
80
- if (reviewFlag === false) {
81
- return false;
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 false;
94
+ return "off";
85
95
  }
86
96
  if (!process.stdin.isTTY) {
87
- return false;
97
+ return "off";
88
98
  }
89
99
  const { default: select } = await import("@inquirer/select");
90
- const mode = await select({
100
+ return select({
91
101
  message: "Quality mode:",
92
102
  choices: [
93
- { name: "Standard — current behavior (no extra cost)", value: "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: "standard",
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.reviewEnabled) {
129
- env.REVIEW_ENABLED = "true";
130
- env.REVIEW_INTERVAL = "5";
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, reviewEnabled) {
5
+ export function renderStatusBar(ralph, reviewMode) {
6
6
  const pid = ralph.child.pid ?? "?";
7
- const badge = reviewEnabled ? " [review]" : "";
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, reviewEnabled } = options;
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, reviewEnabled);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bmalph",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "Unified AI Development Framework - BMAD phases with Ralph execution loop",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.
@@ -46,7 +46,7 @@ driver_permission_denial_help() {
46
46
  }
47
47
 
48
48
  # Build Codex CLI command
49
- # Codex uses: codex exec [--resume <id>] --json "prompt"
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+=("--resume" "$session_id")
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 -qi "$keyword"; then
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 -qi "$pattern"; then
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
- # Track whether an explicit EXIT_SIGNAL was found in RALPH_STATUS block
1039
- # If explicit signal found, heuristics should NOT override Claude's intent
1040
- local explicit_exit_signal_found=false
1041
-
1042
- # 1. Check for explicit structured output (if Claude follows schema)
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 - Claude says to continue
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
- # 2. Detect completion keywords in natural language output
1083
- for keyword in "${COMPLETION_KEYWORDS[@]}"; do
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
- # 3. Detect test-only loops
1092
- local test_command_count=0
1093
- local implementation_count=0
1094
- local error_count=0
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
- test_command_count=$(grep -c -i "running tests\|npm test\|bats\|pytest\|jest" "$output_file" 2>/dev/null | head -1 || echo "0")
1097
- implementation_count=$(grep -c -i "implementing\|creating\|writing\|adding\|function\|class" "$output_file" 2>/dev/null | head -1 || echo "0")
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
- # Strip whitespace and ensure it's a number
1100
- test_command_count=$(echo "$test_command_count" | tr -d '[:space:]')
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
- # Convert to integers with default fallback
1104
- test_command_count=${test_command_count:-0}
1105
- implementation_count=${implementation_count:-0}
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
- if [[ $test_command_count -gt 0 ]] && [[ $implementation_count -eq 0 ]]; then
1110
- is_test_only=true
1111
- work_summary="Test execution only, no implementation"
1112
- fi
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
- # 4. Detect stuck/error loops
1115
- # Use two-stage filtering to avoid counting JSON field names as errors
1116
- # Stage 1: Filter out JSON field patterns like "is_error": false
1117
- # Stage 2: Count actual error messages in specific contexts
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
- if [[ $error_count -gt 5 ]]; then
1127
- is_stuck=true
1128
- fi
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
- # 5. Detect "nothing to do" patterns
1131
- for pattern in "${NO_WORK_PATTERNS[@]}"; do
1132
- if grep -qi "$pattern" "$output_file"; then
1133
- has_completion_signal=true
1134
- ((confidence_score+=15))
1135
- work_summary="No work remaining"
1136
- break
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
- # 6. Check for file changes (git integration)
1141
- # Fix #141: Detect both uncommitted changes AND committed changes
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
- # 7. Analyze output length trends (detect declining engagement)
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='[]'
@@ -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:-$HOME/.ralph}"
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 periodic code review should run this iteration
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
- [[ "$REVIEW_ENABLED" != "true" ]] && return 1
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
- (( loop_count % REVIEW_INTERVAL != 0 )) && return 1
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
- local context="REVIEW FINDINGS ($severity, $issues_found issues): $summary"
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:500}"
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
- # Periodic code review check
2503
- if should_run_review "$loop_count"; then
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
- # Enable periodic code review loops (set via 'bmalph run --review' or manually)
133
- # When enabled, Ralph runs a read-only review session every REVIEW_INTERVAL loops.
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 (default: 5)
143
+ # Number of implementation loops between review sessions (enhanced mode only)
139
144
  REVIEW_INTERVAL="${REVIEW_INTERVAL:-5}"
140
145
 
141
146
  # =============================================================================