bmalph 2.7.4 → 2.7.5
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 +68 -30
- package/dist/commands/doctor-checks.js +5 -4
- package/dist/commands/doctor-checks.js.map +1 -1
- package/dist/commands/run.js +4 -0
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/status.js +12 -3
- package/dist/commands/status.js.map +1 -1
- package/dist/installer.js +101 -48
- package/dist/installer.js.map +1 -1
- package/dist/platform/cursor-runtime-checks.js +81 -0
- package/dist/platform/cursor-runtime-checks.js.map +1 -0
- package/dist/platform/cursor.js +4 -3
- package/dist/platform/cursor.js.map +1 -1
- package/dist/platform/detect.js +28 -5
- package/dist/platform/detect.js.map +1 -1
- package/dist/platform/instructions-snippet.js +18 -0
- package/dist/platform/instructions-snippet.js.map +1 -1
- package/dist/platform/resolve.js +23 -5
- package/dist/platform/resolve.js.map +1 -1
- package/dist/run/ralph-process.js +84 -15
- package/dist/run/ralph-process.js.map +1 -1
- package/dist/transition/artifact-scan.js +15 -3
- package/dist/transition/artifact-scan.js.map +1 -1
- package/package.json +1 -1
- package/ralph/RALPH-REFERENCE.md +27 -3
- package/ralph/drivers/cursor.sh +47 -29
- package/ralph/lib/response_analyzer.sh +93 -10
package/package.json
CHANGED
package/ralph/RALPH-REFERENCE.md
CHANGED
|
@@ -85,13 +85,19 @@ Ralph maintains session continuity across loop iterations using `--resume` with
|
|
|
85
85
|
|
|
86
86
|
Ralph uses `--resume <session_id>` instead of `--continue` to resume sessions. This ensures Ralph only resumes its own saved sessions and avoids hijacking unrelated active sessions.
|
|
87
87
|
|
|
88
|
+
This applies to every driver that exposes resumable IDs today:
|
|
89
|
+
|
|
90
|
+
- Claude Code
|
|
91
|
+
- OpenAI Codex
|
|
92
|
+
- Cursor
|
|
93
|
+
|
|
88
94
|
### Session Files
|
|
89
95
|
|
|
90
96
|
| File | Purpose |
|
|
91
97
|
|------|---------|
|
|
92
98
|
| `.ralph/.ralph_session` | Current session ID and timestamps |
|
|
93
99
|
| `.ralph/.ralph_session_history` | History of last 50 session transitions |
|
|
94
|
-
| `.ralph/.claude_session_id` | Persisted driver session ID (shared filename for historical reasons) |
|
|
100
|
+
| `.ralph/.claude_session_id` | Persisted driver session ID (shared filename for historical reasons; used by Claude Code, Codex, and Cursor) |
|
|
95
101
|
|
|
96
102
|
### Session Lifecycle
|
|
97
103
|
|
|
@@ -256,7 +262,9 @@ bash .ralph/ralph_loop.sh --monitor --live # Live streaming with tmux monitoring
|
|
|
256
262
|
|
|
257
263
|
### How It Works
|
|
258
264
|
|
|
259
|
-
-
|
|
265
|
+
- Live mode switches the active driver to its structured streaming format and pipes the stream through `jq`
|
|
266
|
+
- Cursor background loop execution stays on `json` output and switches to `stream-json` for live display
|
|
267
|
+
- Claude Code also uses `stream-json` for live display, while Codex streams its native JSONL events directly
|
|
260
268
|
- Shows text deltas and tool invocations in real-time
|
|
261
269
|
- Requires `jq` and `stdbuf` (from coreutils); falls back to background mode if unavailable
|
|
262
270
|
|
|
@@ -341,6 +349,20 @@ When using `--monitor` with `--live`, tmux creates a 3-pane layout:
|
|
|
341
349
|
2. Start a new session with `bash .ralph/ralph_loop.sh --reset-session`
|
|
342
350
|
3. Context will be rebuilt from `@fix_plan.md` and `specs/`
|
|
343
351
|
|
|
352
|
+
#### Cursor preflight fails
|
|
353
|
+
|
|
354
|
+
**Symptoms:** `bmalph doctor` or `bmalph run --driver cursor` fails before the loop starts
|
|
355
|
+
|
|
356
|
+
**Causes:**
|
|
357
|
+
- `command -v jq` fails in the bash environment Ralph uses
|
|
358
|
+
- `command -v cursor-agent` fails in that same bash environment
|
|
359
|
+
- `cursor-agent status` reports an authentication problem
|
|
360
|
+
|
|
361
|
+
**Solutions:**
|
|
362
|
+
1. Run `command -v jq` in the same bash shell Ralph uses and install `jq` if missing
|
|
363
|
+
2. Run `command -v cursor-agent` and ensure the official Cursor CLI is on the bash `PATH`
|
|
364
|
+
3. Run `cursor-agent status` and sign in to Cursor before starting Ralph
|
|
365
|
+
|
|
344
366
|
### Diagnostic Commands
|
|
345
367
|
|
|
346
368
|
```bash
|
|
@@ -383,12 +405,14 @@ Loop execution logs are stored in `.ralph/logs/`:
|
|
|
383
405
|
"calls_made_this_hour": 25,
|
|
384
406
|
"max_calls_per_hour": 100,
|
|
385
407
|
"last_action": "description",
|
|
386
|
-
"status": "running|paused|
|
|
408
|
+
"status": "running|completed|halted|paused|stopped|success|graceful_exit|error",
|
|
387
409
|
"exit_reason": "reason (if exited)",
|
|
388
410
|
"next_reset": "timestamp for rate limit reset"
|
|
389
411
|
}
|
|
390
412
|
```
|
|
391
413
|
|
|
414
|
+
`bmalph status` normalizes these raw bash values to `running`, `blocked`, `completed`, `not_started`, or `unknown`.
|
|
415
|
+
|
|
392
416
|
---
|
|
393
417
|
|
|
394
418
|
## Error Detection
|
package/ralph/drivers/cursor.sh
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Cursor CLI driver for Ralph
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# Known limitations:
|
|
6
|
-
# - CLI is in beta — binary name and flags may change
|
|
7
|
-
# - NDJSON stream format assumes {type: "text", content: "..."} events
|
|
8
|
-
# - Session continuity is disabled until Cursor exposes a stable capturable session ID
|
|
2
|
+
# Cursor CLI driver for Ralph
|
|
3
|
+
# Uses the documented cursor-agent contract for background execution and
|
|
4
|
+
# switches to stream-json only for live display paths.
|
|
9
5
|
|
|
10
6
|
driver_name() {
|
|
11
7
|
echo "cursor"
|
|
@@ -42,7 +38,6 @@ driver_check_available() {
|
|
|
42
38
|
command -v "$cli_binary" &>/dev/null
|
|
43
39
|
}
|
|
44
40
|
|
|
45
|
-
# Cursor CLI tool names
|
|
46
41
|
driver_valid_tools() {
|
|
47
42
|
VALID_TOOL_PATTERNS=(
|
|
48
43
|
"file_edit"
|
|
@@ -53,10 +48,6 @@ driver_valid_tools() {
|
|
|
53
48
|
)
|
|
54
49
|
}
|
|
55
50
|
|
|
56
|
-
# Build Cursor CLI command
|
|
57
|
-
# Context is prepended to the prompt (same pattern as Codex/Copilot drivers).
|
|
58
|
-
# Uses --print for headless mode, --force for autonomous execution,
|
|
59
|
-
# --output-format stream-json for NDJSON streaming.
|
|
60
51
|
driver_build_command() {
|
|
61
52
|
local prompt_file=$1
|
|
62
53
|
local loop_context=$2
|
|
@@ -76,16 +67,12 @@ driver_build_command() {
|
|
|
76
67
|
CLAUDE_CMD_ARGS+=("$cli_binary")
|
|
77
68
|
fi
|
|
78
69
|
|
|
79
|
-
|
|
80
|
-
CLAUDE_CMD_ARGS+=("--print")
|
|
70
|
+
CLAUDE_CMD_ARGS+=("-p" "--force" "--output-format" "json")
|
|
81
71
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# NDJSON streaming output
|
|
86
|
-
CLAUDE_CMD_ARGS+=("--output-format" "stream-json")
|
|
72
|
+
if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then
|
|
73
|
+
CLAUDE_CMD_ARGS+=("--resume" "$session_id")
|
|
74
|
+
fi
|
|
87
75
|
|
|
88
|
-
# Build prompt with context prepended
|
|
89
76
|
local prompt_content
|
|
90
77
|
if driver_running_on_windows; then
|
|
91
78
|
prompt_content=$(driver_build_windows_bootstrap_prompt "$loop_context" "$prompt_file")
|
|
@@ -102,20 +89,43 @@ $prompt_content"
|
|
|
102
89
|
}
|
|
103
90
|
|
|
104
91
|
driver_supports_sessions() {
|
|
105
|
-
return
|
|
92
|
+
return 0
|
|
106
93
|
}
|
|
107
94
|
|
|
108
95
|
driver_supports_live_output() {
|
|
109
|
-
return 0
|
|
96
|
+
return 0
|
|
110
97
|
}
|
|
111
98
|
|
|
112
99
|
driver_prepare_live_command() {
|
|
113
|
-
LIVE_CMD_ARGS=(
|
|
100
|
+
LIVE_CMD_ARGS=()
|
|
101
|
+
local skip_next=false
|
|
102
|
+
|
|
103
|
+
for arg in "${CLAUDE_CMD_ARGS[@]}"; do
|
|
104
|
+
if [[ "$skip_next" == "true" ]]; then
|
|
105
|
+
LIVE_CMD_ARGS+=("stream-json")
|
|
106
|
+
skip_next=false
|
|
107
|
+
elif [[ "$arg" == "--output-format" ]]; then
|
|
108
|
+
LIVE_CMD_ARGS+=("$arg")
|
|
109
|
+
skip_next=true
|
|
110
|
+
else
|
|
111
|
+
LIVE_CMD_ARGS+=("$arg")
|
|
112
|
+
fi
|
|
113
|
+
done
|
|
114
|
+
|
|
115
|
+
if [[ "$skip_next" == "true" ]]; then
|
|
116
|
+
return 1
|
|
117
|
+
fi
|
|
114
118
|
}
|
|
115
119
|
|
|
116
|
-
# Cursor CLI outputs NDJSON events
|
|
117
120
|
driver_stream_filter() {
|
|
118
|
-
echo '
|
|
121
|
+
echo '
|
|
122
|
+
if .type == "assistant" then
|
|
123
|
+
[(.message.content[]? | select(.type == "text") | .text)] | join("\n")
|
|
124
|
+
elif .type == "tool_call" then
|
|
125
|
+
"\n\n⚡ [" + (.tool_call.name // .name // "tool_call") + "]\n"
|
|
126
|
+
else
|
|
127
|
+
empty
|
|
128
|
+
end'
|
|
119
129
|
}
|
|
120
130
|
|
|
121
131
|
driver_running_on_windows() {
|
|
@@ -208,10 +218,18 @@ driver_localappdata_cli_binary() {
|
|
|
208
218
|
local_app_data=$(cygpath -u "$local_app_data")
|
|
209
219
|
fi
|
|
210
220
|
|
|
211
|
-
local
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
221
|
+
local candidates=(
|
|
222
|
+
"$local_app_data/cursor-agent/cursor-agent.cmd"
|
|
223
|
+
"$local_app_data/cursor-agent/agent.cmd"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
local candidate
|
|
227
|
+
for candidate in "${candidates[@]}"; do
|
|
228
|
+
if [[ -f "$candidate" ]]; then
|
|
229
|
+
echo "$candidate"
|
|
230
|
+
return 0
|
|
231
|
+
fi
|
|
232
|
+
done
|
|
215
233
|
}
|
|
216
234
|
|
|
217
235
|
driver_wrapper_path() {
|
|
@@ -97,6 +97,37 @@ normalize_codex_jsonl_response() {
|
|
|
97
97
|
' "$output_file" > "$normalized_file"
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
# Normalize Cursor stream-json event output into the object shape expected downstream.
|
|
101
|
+
normalize_cursor_stream_json_response() {
|
|
102
|
+
local output_file=$1
|
|
103
|
+
local normalized_file=$2
|
|
104
|
+
|
|
105
|
+
jq -rs '
|
|
106
|
+
def assistant_text($item):
|
|
107
|
+
[($item.message.content // [])[]? | select(.type == "text") | .text]
|
|
108
|
+
| join("\n");
|
|
109
|
+
|
|
110
|
+
(map(select(.type == "result")) | last // {}) as $result_event
|
|
111
|
+
| {
|
|
112
|
+
result: (
|
|
113
|
+
$result_event.result
|
|
114
|
+
// (
|
|
115
|
+
map(select(.type == "assistant"))
|
|
116
|
+
| map(assistant_text(.))
|
|
117
|
+
| map(select(length > 0))
|
|
118
|
+
| join("\n")
|
|
119
|
+
)
|
|
120
|
+
),
|
|
121
|
+
sessionId: (
|
|
122
|
+
$result_event.session_id
|
|
123
|
+
// (map(select(.type == "system" and .subtype == "init") | .session_id // empty) | first)
|
|
124
|
+
// ""
|
|
125
|
+
),
|
|
126
|
+
metadata: {}
|
|
127
|
+
}
|
|
128
|
+
' "$output_file" > "$normalized_file"
|
|
129
|
+
}
|
|
130
|
+
|
|
100
131
|
# Detect whether a multi-document stream matches Codex JSONL events.
|
|
101
132
|
is_codex_jsonl_output() {
|
|
102
133
|
local output_file=$1
|
|
@@ -112,6 +143,24 @@ is_codex_jsonl_output() {
|
|
|
112
143
|
' < "$output_file" 2>/dev/null
|
|
113
144
|
}
|
|
114
145
|
|
|
146
|
+
# Detect whether a multi-document stream matches Cursor stream-json events.
|
|
147
|
+
is_cursor_stream_json_output() {
|
|
148
|
+
local output_file=$1
|
|
149
|
+
|
|
150
|
+
jq -n -j '
|
|
151
|
+
reduce inputs as $item (
|
|
152
|
+
false;
|
|
153
|
+
. or (
|
|
154
|
+
$item.type == "system" or
|
|
155
|
+
$item.type == "user" or
|
|
156
|
+
$item.type == "assistant" or
|
|
157
|
+
$item.type == "tool_call" or
|
|
158
|
+
$item.type == "result"
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
' < "$output_file" 2>/dev/null
|
|
162
|
+
}
|
|
163
|
+
|
|
115
164
|
# Normalize structured output to a single object when downstream parsing expects one.
|
|
116
165
|
normalize_json_output() {
|
|
117
166
|
local output_file=$1
|
|
@@ -129,6 +178,14 @@ normalize_json_output() {
|
|
|
129
178
|
return $?
|
|
130
179
|
fi
|
|
131
180
|
|
|
181
|
+
local is_cursor_stream_json
|
|
182
|
+
is_cursor_stream_json=$(is_cursor_stream_json_output "$output_file") || return 1
|
|
183
|
+
|
|
184
|
+
if [[ "$is_cursor_stream_json" == "true" ]]; then
|
|
185
|
+
normalize_cursor_stream_json_response "$output_file" "$normalized_file"
|
|
186
|
+
return $?
|
|
187
|
+
fi
|
|
188
|
+
|
|
132
189
|
return 1
|
|
133
190
|
fi
|
|
134
191
|
|
|
@@ -215,25 +272,45 @@ detect_output_format() {
|
|
|
215
272
|
|
|
216
273
|
if [[ "$is_codex_jsonl" == "true" ]]; then
|
|
217
274
|
echo "json"
|
|
218
|
-
|
|
275
|
+
return
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
local is_cursor_stream_json
|
|
279
|
+
is_cursor_stream_json=$(is_cursor_stream_json_output "$output_file") || {
|
|
219
280
|
echo "text"
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if [[ "$is_cursor_stream_json" == "true" ]]; then
|
|
285
|
+
echo "json"
|
|
286
|
+
return
|
|
220
287
|
fi
|
|
221
|
-
return
|
|
222
288
|
fi
|
|
223
289
|
|
|
224
290
|
echo "text"
|
|
225
291
|
}
|
|
226
292
|
|
|
293
|
+
trim_shell_whitespace() {
|
|
294
|
+
local value="${1//$'\r'/}"
|
|
295
|
+
|
|
296
|
+
value="${value#"${value%%[![:space:]]*}"}"
|
|
297
|
+
value="${value%"${value##*[![:space:]]}"}"
|
|
298
|
+
|
|
299
|
+
printf '%s' "$value"
|
|
300
|
+
}
|
|
301
|
+
|
|
227
302
|
# Parse JSON response and extract structured fields
|
|
228
303
|
# Creates .ralph/.json_parse_result with normalized analysis data
|
|
229
|
-
# Supports
|
|
304
|
+
# Supports FIVE JSON formats:
|
|
230
305
|
# 1. Flat format: { status, exit_signal, work_type, files_modified, ... }
|
|
231
306
|
# 2. Claude CLI object format: { result, sessionId, metadata: { files_changed, has_errors, completion_status, ... } }
|
|
232
307
|
# 3. Claude CLI array format: [ {type: "system", ...}, {type: "assistant", ...}, {type: "result", ...} ]
|
|
233
308
|
# 4. Codex JSONL format: {"type":"thread.started",...}\n{"type":"item.completed","item":{...}}
|
|
309
|
+
# 5. Cursor stream-json format: {"type":"assistant",...}\n{"type":"result",...}
|
|
234
310
|
parse_json_response() {
|
|
235
311
|
local output_file=$1
|
|
236
312
|
local result_file="${2:-$RALPH_DIR/.json_parse_result}"
|
|
313
|
+
local original_output_file=$output_file
|
|
237
314
|
local normalized_file=""
|
|
238
315
|
local json_document_count=""
|
|
239
316
|
local response_shape="object"
|
|
@@ -267,7 +344,11 @@ parse_json_response() {
|
|
|
267
344
|
output_file="$normalized_file"
|
|
268
345
|
|
|
269
346
|
if [[ "$response_shape" == "jsonl" ]]; then
|
|
270
|
-
|
|
347
|
+
if is_codex_jsonl_output "$original_output_file" >/dev/null 2>&1; then
|
|
348
|
+
response_shape="codex_jsonl"
|
|
349
|
+
else
|
|
350
|
+
response_shape="cursor_stream_jsonl"
|
|
351
|
+
fi
|
|
271
352
|
fi
|
|
272
353
|
fi
|
|
273
354
|
|
|
@@ -296,7 +377,7 @@ parse_json_response() {
|
|
|
296
377
|
if [[ -n "$result_text" ]] && echo "$result_text" | grep -q -- "---RALPH_STATUS---"; then
|
|
297
378
|
# Extract EXIT_SIGNAL value from RALPH_STATUS block within result text
|
|
298
379
|
local embedded_exit_sig
|
|
299
|
-
embedded_exit_sig=$(
|
|
380
|
+
embedded_exit_sig=$(trim_shell_whitespace "$(printf '%s\n' "$result_text" | grep "EXIT_SIGNAL:" | cut -d: -f2)")
|
|
300
381
|
if [[ -n "$embedded_exit_sig" ]]; then
|
|
301
382
|
# Explicit EXIT_SIGNAL found in RALPH_STATUS block
|
|
302
383
|
explicit_exit_signal_found="true"
|
|
@@ -311,7 +392,7 @@ parse_json_response() {
|
|
|
311
392
|
# Also check STATUS field as fallback ONLY when EXIT_SIGNAL was not specified
|
|
312
393
|
# This respects explicit EXIT_SIGNAL: false which means "task complete, continue working"
|
|
313
394
|
local embedded_status
|
|
314
|
-
embedded_status=$(
|
|
395
|
+
embedded_status=$(trim_shell_whitespace "$(printf '%s\n' "$result_text" | grep "STATUS:" | cut -d: -f2)")
|
|
315
396
|
if [[ "$embedded_status" == "COMPLETE" && "$explicit_exit_signal_found" != "true" ]]; then
|
|
316
397
|
# STATUS: COMPLETE without any EXIT_SIGNAL field implies completion
|
|
317
398
|
exit_signal="true"
|
|
@@ -341,7 +422,7 @@ parse_json_response() {
|
|
|
341
422
|
local summary=$(jq -r -j '.result // .summary // ""' "$output_file" 2>/dev/null)
|
|
342
423
|
|
|
343
424
|
# Session ID: from Claude CLI format (sessionId) OR from metadata.session_id
|
|
344
|
-
local session_id=$(jq -r -j '.sessionId // .metadata.session_id // ""' "$output_file" 2>/dev/null)
|
|
425
|
+
local session_id=$(jq -r -j '.sessionId // .metadata.session_id // .session_id // ""' "$output_file" 2>/dev/null)
|
|
345
426
|
|
|
346
427
|
# Loop number: from metadata
|
|
347
428
|
local loop_number=$(jq -r -j '.metadata.loop_number // .loop_number // 0' "$output_file" 2>/dev/null)
|
|
@@ -375,7 +456,7 @@ parse_json_response() {
|
|
|
375
456
|
# completion markers are absent. This keeps JSONL analysis aligned with text mode.
|
|
376
457
|
local summary_has_completion_keyword="false"
|
|
377
458
|
local summary_has_no_work_pattern="false"
|
|
378
|
-
if [[ "$response_shape" == "codex_jsonl" && "$explicit_exit_signal_found" != "true" && -n "$summary" ]]; then
|
|
459
|
+
if [[ "$response_shape" == "codex_jsonl" || "$response_shape" == "cursor_stream_jsonl" ]] && [[ "$explicit_exit_signal_found" != "true" && -n "$summary" ]]; then
|
|
379
460
|
for keyword in "${COMPLETION_KEYWORDS[@]}"; do
|
|
380
461
|
if echo "$summary" | grep -qi "$keyword"; then
|
|
381
462
|
summary_has_completion_keyword="true"
|
|
@@ -639,8 +720,8 @@ analyze_response() {
|
|
|
639
720
|
# 1. Check for explicit structured output (if Claude follows schema)
|
|
640
721
|
if grep -q -- "---RALPH_STATUS---" "$output_file"; then
|
|
641
722
|
# Parse structured output
|
|
642
|
-
local status=$(grep "STATUS:" "$output_file" | cut -d: -f2
|
|
643
|
-
local exit_sig=$(grep "EXIT_SIGNAL:" "$output_file" | cut -d: -f2
|
|
723
|
+
local status=$(trim_shell_whitespace "$(grep "STATUS:" "$output_file" | cut -d: -f2)")
|
|
724
|
+
local exit_sig=$(trim_shell_whitespace "$(grep "EXIT_SIGNAL:" "$output_file" | cut -d: -f2)")
|
|
644
725
|
|
|
645
726
|
# If EXIT_SIGNAL is explicitly provided, respect it
|
|
646
727
|
if [[ -n "$exit_sig" ]]; then
|
|
@@ -1093,7 +1174,9 @@ export -f detect_output_format
|
|
|
1093
1174
|
export -f count_json_documents
|
|
1094
1175
|
export -f normalize_cli_array_response
|
|
1095
1176
|
export -f normalize_codex_jsonl_response
|
|
1177
|
+
export -f normalize_cursor_stream_json_response
|
|
1096
1178
|
export -f is_codex_jsonl_output
|
|
1179
|
+
export -f is_cursor_stream_json_output
|
|
1097
1180
|
export -f normalize_json_output
|
|
1098
1181
|
export -f extract_session_id_from_output
|
|
1099
1182
|
export -f parse_json_response
|